blanchon commited on
Commit
f62f94b
·
1 Parent(s): 838a719
packages/feetech.js/README.md CHANGED
@@ -1,8 +1,10 @@
1
  # feetech.js
2
 
 
 
3
  Control feetech servos through browser
4
 
5
- ## Usage
6
 
7
  ```bash
8
  # Install the package
@@ -10,15 +12,29 @@ npm install feetech.js
10
  ```
11
 
12
  ```javascript
13
- import { scsServoSDK } from "feetech.js";
 
14
 
15
- await scsServoSDK.connect();
 
 
16
 
17
- const position = await scsServoSDK.readPosition(1);
 
18
  console.log(position); // 1122
19
  ```
20
 
 
 
 
 
 
21
  ## Example usage:
22
 
23
- - simple example: [test.html](./test.html)
24
- - the bambot website: [bambot.org](https://bambot.org)
 
 
 
 
 
 
1
  # feetech.js
2
 
3
+ > [bambot.org/feetech.js](https://bambot.org/feetech.js)
4
+
5
  Control feetech servos through browser
6
 
7
+ ## Quick start
8
 
9
  ```bash
10
  # Install the package
 
12
  ```
13
 
14
  ```javascript
15
+ import { ScsServoSDK } from "feetech.js";
16
+ const scsServoSdk = new ScsServoSDK();
17
 
18
+ // request permission to access the USB device and connect to it
19
+ // Note: This will prompt the user to select a USB device
20
+ await scsServoSdk.connect();
21
 
22
+ // read servo position
23
+ const position = await scsServoSdk.readPosition(1);
24
  console.log(position); // 1122
25
  ```
26
 
27
+ ## Documentations
28
+
29
+ https://deepwiki.com/timqian/bambot/4.1-feetech.js-sdk
30
+
31
+
32
  ## Example usage:
33
 
34
+ - Test and config servos: [bambot.org/feetech.js](https://bambot.org/feetech.js)
35
+ - Simple html + js example: [test.html](https://github.com/timqian/bambot/blob/main/feetech.js/test.html)
36
+ - Control different bots: [bambot.org](https://bambot.org)
37
+
38
+ ## Ref
39
+
40
+ - https://github.com/Adam-Software/Feetech-Servo-SDK
packages/feetech.js/debug.mjs DELETED
@@ -1,15 +0,0 @@
1
- /**
2
- * Debug configuration for feetech.js
3
- * Set DEBUG_ENABLED to false to disable all console.log statements for performance
4
- */
5
- export const DEBUG_ENABLED = true; // Set to true to enable debug logging
6
-
7
- /**
8
- * Conditional logging function that respects the DEBUG_ENABLED flag
9
- * @param {...any} args - Arguments to log
10
- */
11
- export const debugLog = (...args) => {
12
- if (DEBUG_ENABLED) {
13
- console.log(...args);
14
- }
15
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
packages/feetech.js/index.d.ts CHANGED
@@ -1,50 +1,28 @@
1
  export type ConnectionOptions = {
2
- baudRate?: number;
3
- protocolEnd?: number;
4
  };
5
 
6
  export type ServoPositions = Map<number, number> | Record<number, number>;
7
- export type ServoSpeeds = Map<number, number> | Record<number, number>;
8
-
9
- export interface ScsServoSDK {
10
- // Connection management
11
- connect(options?: ConnectionOptions): Promise<true>;
12
- disconnect(): Promise<true>;
13
- isConnected(): boolean;
14
-
15
- // Servo locking operations
16
- lockServo(servoId: number): Promise<"success">;
17
- unlockServo(servoId: number): Promise<"success">;
18
- lockServos(servoIds: number[]): Promise<"success">;
19
- unlockServos(servoIds: number[]): Promise<"success">;
20
- lockServosForProduction(servoIds: number[]): Promise<"success">;
21
- unlockServosForManualMovement(servoIds: number[]): Promise<"success">;
22
-
23
- // Read operations (no locking needed)
24
- readPosition(servoId: number): Promise<number>;
25
- syncReadPositions(servoIds: number[]): Promise<Map<number, number>>;
26
-
27
- // Write operations - LOCKED MODE (respects servo locks)
28
- writePosition(servoId: number, position: number): Promise<"success">;
29
- writeTorqueEnable(servoId: number, enable: boolean): Promise<"success">;
30
-
31
- // Write operations - UNLOCKED MODE (temporary unlock for operation)
32
- writePositionUnlocked(servoId: number, position: number): Promise<"success">;
33
- writePositionAndDisableTorque(servoId: number, position: number, waitTimeMs?: number): Promise<"success">;
34
- writeTorqueEnableUnlocked(servoId: number, enable: boolean): Promise<"success">;
35
-
36
- // Sync write operations
37
- syncWritePositions(servoPositions: ServoPositions): Promise<"success">;
38
-
39
- // Configuration functions
40
- setBaudRate(servoId: number, baudRateIndex: number): Promise<"success">;
41
- setServoId(currentServoId: number, newServoId: number): Promise<"success">;
42
- setWheelMode(servoId: number): Promise<"success">;
43
- setPositionMode(servoId: number): Promise<"success">;
44
  }
45
 
46
- export const scsServoSDK: ScsServoSDK;
47
-
48
- // Debug exports
49
- export const DEBUG_ENABLED: boolean;
50
- export function debugLog(message: string): void;
 
1
  export type ConnectionOptions = {
2
+ baudRate?: number;
3
+ protocolEnd?: number;
4
  };
5
 
6
  export type ServoPositions = Map<number, number> | Record<number, number>;
7
+ export type ServoSpeeds = Map<number, number> | Record<number, number>; // New type alias for speeds
8
+
9
+ export declare class ScsServoSDK {
10
+ connect(options?: ConnectionOptions): Promise<true>;
11
+ disconnect(): Promise<true>;
12
+ readPosition(servoId: number): Promise<number>;
13
+ readBaudRate(servoId: number): Promise<number>;
14
+ readMode(servoId: number): Promise<number>;
15
+ writePosition(servoId: number, position: number): Promise<"success">;
16
+ writeTorqueEnable(servoId: number, enable: boolean): Promise<"success">;
17
+ writeAcceleration(servoId: number, acceleration: number): Promise<"success">;
18
+ setWheelMode(servoId: number): Promise<"success">;
19
+ setPositionMode(servoId: number): Promise<"success">;
20
+ writeWheelSpeed(servoId: number, speed: number): Promise<"success">;
21
+ syncReadPositions(servoIds: number[]): Promise<Map<number, number>>;
22
+ syncWritePositions(servoPositions: ServoPositions): Promise<"success">;
23
+ syncWriteWheelSpeed(servoSpeeds: ServoSpeeds): Promise<"success">;
24
+ setBaudRate(servoId: number, baudRateIndex: number): Promise<"success">;
25
+ setServoId(currentServoId: number, newServoId: number): Promise<"success">;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  }
27
 
28
+ export declare const scsServoSDK: ScsServoSDK;
 
 
 
 
packages/feetech.js/index.mjs CHANGED
@@ -1,65 +1,8 @@
1
- // Import all functions from the unified scsServoSDK module
2
- import {
3
- connect,
4
- disconnect,
5
- isConnected,
6
- lockServo,
7
- unlockServo,
8
- lockServos,
9
- unlockServos,
10
- lockServosForProduction,
11
- unlockServosForManualMovement,
12
- readPosition,
13
- syncReadPositions,
14
- writePosition,
15
- writeTorqueEnable,
16
- writePositionUnlocked,
17
- writePositionAndDisableTorque,
18
- writeTorqueEnableUnlocked,
19
- syncWritePositions,
20
- setBaudRate,
21
- setServoId,
22
- setWheelMode,
23
- setPositionMode
24
- } from "./scsServoSDK.mjs";
25
 
26
- // Create the unified SCS servo SDK object
27
- export const scsServoSDK = {
28
- // Connection management
29
- connect,
30
- disconnect,
31
- isConnected,
32
 
33
- // Servo locking operations
34
- lockServo,
35
- unlockServo,
36
- lockServos,
37
- unlockServos,
38
- lockServosForProduction,
39
- unlockServosForManualMovement,
40
-
41
- // Read operations (no locking needed)
42
- readPosition,
43
- syncReadPositions,
44
-
45
- // Write operations - LOCKED MODE (respects servo locks)
46
- writePosition,
47
- writeTorqueEnable,
48
-
49
- // Write operations - UNLOCKED MODE (temporary unlock for operation)
50
- writePositionUnlocked,
51
- writePositionAndDisableTorque,
52
- writeTorqueEnableUnlocked,
53
-
54
- // Sync write operations
55
- syncWritePositions,
56
-
57
- // Configuration functions
58
- setBaudRate,
59
- setServoId,
60
- setWheelMode,
61
- setPositionMode
62
- };
63
-
64
- // Export debug configuration for easy access
65
- export { DEBUG_ENABLED, debugLog } from "./debug.mjs";
 
1
+ // Import all functions from the new scsServoSDK module
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
+ // Export the class and singleton instance
4
+ export { ScsServoSDK, scsServoSDK } from "./scsServoSDK.mjs";
 
 
 
 
5
 
6
+ // Future: You can add exports for other servo types here, e.g.:
7
+ // export { stsServoSDK } from './stsServoSDK.mjs';
8
+ // export { smsServoSDK } from './smsServoSDK.mjs';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
packages/feetech.js/lowLevelSDK.mjs CHANGED
@@ -1,9 +1,6 @@
1
- // Import debug logging function
2
- import { debugLog } from "./debug.mjs";
3
-
4
  // Constants
5
- export const BROADCAST_ID = 0xfe; // 254
6
- export const MAX_ID = 0xfc; // 252
7
 
8
  // Protocol instructions
9
  export const INST_PING = 1;
@@ -11,19 +8,19 @@ export const INST_READ = 2;
11
  export const INST_WRITE = 3;
12
  export const INST_REG_WRITE = 4;
13
  export const INST_ACTION = 5;
14
- export const INST_SYNC_WRITE = 131; // 0x83
15
- export const INST_SYNC_READ = 130; // 0x82
16
- export const INST_STATUS = 85; // 0x55, status packet instruction (0x55)
17
 
18
  // Communication results
19
- export const COMM_SUCCESS = 0; // tx or rx packet communication success
20
- export const COMM_PORT_BUSY = -1; // Port is busy (in use)
21
- export const COMM_TX_FAIL = -2; // Failed transmit instruction packet
22
- export const COMM_RX_FAIL = -3; // Failed get status packet
23
- export const COMM_TX_ERROR = -4; // Incorrect instruction packet
24
- export const COMM_RX_WAITING = -5; // Now receiving status packet
25
- export const COMM_RX_TIMEOUT = -6; // There is no status packet
26
- export const COMM_RX_CORRUPT = -7; // Incorrect status packet
27
  export const COMM_NOT_AVAILABLE = -9;
28
 
29
  // Packet constants
@@ -55,872 +52,848 @@ let SCS_END = 0; // (STS/SMS=0, SCS=1)
55
 
56
  // Utility functions for handling word operations
57
  export function SCS_LOWORD(l) {
58
- return l & 0xffff;
59
  }
60
 
61
  export function SCS_HIWORD(l) {
62
- return (l >> 16) & 0xffff;
63
  }
64
 
65
  export function SCS_LOBYTE(w) {
66
- if (SCS_END === 0) {
67
- return w & 0xff;
68
- } else {
69
- return (w >> 8) & 0xff;
70
- }
71
  }
72
 
73
  export function SCS_HIBYTE(w) {
74
- if (SCS_END === 0) {
75
- return (w >> 8) & 0xff;
76
- } else {
77
- return w & 0xff;
78
- }
79
  }
80
 
81
  export function SCS_MAKEWORD(a, b) {
82
- if (SCS_END === 0) {
83
- return (a & 0xff) | ((b & 0xff) << 8);
84
- } else {
85
- return (b & 0xff) | ((a & 0xff) << 8);
86
- }
87
  }
88
 
89
  export function SCS_MAKEDWORD(a, b) {
90
- return (a & 0xffff) | ((b & 0xffff) << 16);
91
  }
92
 
93
  export function SCS_TOHOST(a, b) {
94
- if (a & (1 << b)) {
95
- return -(a & ~(1 << b));
96
- } else {
97
- return a;
98
- }
99
  }
100
 
101
  export class PortHandler {
102
- constructor() {
103
- this.port = null;
104
- this.reader = null;
105
- this.writer = null;
106
- this.isOpen = false;
107
- this.isUsing = false;
108
- this.baudrate = DEFAULT_BAUDRATE;
109
- this.packetStartTime = 0;
110
- this.packetTimeout = 0;
111
- this.txTimePerByte = 0;
112
- }
113
-
114
- async requestPort() {
115
- try {
116
- this.port = await navigator.serial.requestPort();
117
- return true;
118
- } catch (err) {
119
- console.error("Error requesting serial port:", err);
120
- return false;
121
- }
122
- }
123
-
124
- async openPort() {
125
- if (!this.port) {
126
- return false;
127
- }
128
-
129
- try {
130
- await this.port.open({ baudRate: this.baudrate });
131
- this.reader = this.port.readable.getReader();
132
- this.writer = this.port.writable.getWriter();
133
- this.isOpen = true;
134
- this.txTimePerByte = (1000.0 / this.baudrate) * 10.0;
135
- return true;
136
- } catch (err) {
137
- console.error("Error opening port:", err);
138
- return false;
139
- }
140
- }
141
-
142
- async closePort() {
143
- if (this.reader) {
144
- await this.reader.releaseLock();
145
- this.reader = null;
146
- }
147
-
148
- if (this.writer) {
149
- await this.writer.releaseLock();
150
- this.writer = null;
151
- }
152
-
153
- if (this.port && this.isOpen) {
154
- await this.port.close();
155
- this.isOpen = false;
156
- }
157
- }
158
-
159
- async clearPort() {
160
- if (this.reader) {
161
- await this.reader.releaseLock();
162
- this.reader = this.port.readable.getReader();
163
- }
164
- }
165
-
166
- setBaudRate(baudrate) {
167
- this.baudrate = baudrate;
168
- this.txTimePerByte = (1000.0 / this.baudrate) * 10.0;
169
- return true;
170
- }
171
-
172
- getBaudRate() {
173
- return this.baudrate;
174
- }
175
-
176
- async writePort(data) {
177
- if (!this.isOpen || !this.writer) {
178
- return 0;
179
- }
180
-
181
- try {
182
- await this.writer.write(new Uint8Array(data));
183
- return data.length;
184
- } catch (err) {
185
- console.error("Error writing to port:", err);
186
- return 0;
187
- }
188
- }
189
-
190
- async readPort(length) {
191
- if (!this.isOpen || !this.reader) {
192
- return [];
193
- }
194
-
195
- try {
196
- // Increase timeout for more reliable data reception
197
- const timeoutMs = 500;
198
- let totalBytes = [];
199
- const startTime = performance.now();
200
-
201
- // Continue reading until we get enough bytes or timeout
202
- while (totalBytes.length < length) {
203
- // Create a timeout promise
204
- const timeoutPromise = new Promise((resolve) => {
205
- setTimeout(() => resolve({ value: new Uint8Array(), done: false, timeout: true }), 100); // Short internal timeout
206
- });
207
-
208
- // Race between reading and timeout
209
- const result = await Promise.race([this.reader.read(), timeoutPromise]);
210
-
211
- if (result.timeout) {
212
- // Internal timeout - check if we've exceeded total timeout
213
- if (performance.now() - startTime > timeoutMs) {
214
- debugLog(`readPort total timeout after ${timeoutMs}ms`);
215
- break;
216
- }
217
- continue; // Try reading again
218
- }
219
-
220
- if (result.done) {
221
- debugLog("Reader done, stream closed");
222
- break;
223
- }
224
-
225
- if (result.value.length === 0) {
226
- // If there's no data but we haven't timed out yet, wait briefly and try again
227
- await new Promise((resolve) => setTimeout(resolve, 10));
228
-
229
- // Check if we've exceeded total timeout
230
- if (performance.now() - startTime > timeoutMs) {
231
- debugLog(`readPort total timeout after ${timeoutMs}ms`);
232
- break;
233
- }
234
- continue;
235
- }
236
-
237
- // Add received bytes to our total
238
- const newData = Array.from(result.value);
239
- totalBytes.push(...newData);
240
- debugLog(
241
- `Read ${newData.length} bytes:`,
242
- newData.map((b) => b.toString(16).padStart(2, "0")).join(" ")
243
- );
244
-
245
- // If we've got enough data, we can stop
246
- if (totalBytes.length >= length) {
247
- break;
248
- }
249
- }
250
-
251
- return totalBytes;
252
- } catch (err) {
253
- console.error("Error reading from port:", err);
254
- return [];
255
- }
256
- }
257
-
258
- setPacketTimeout(packetLength) {
259
- this.packetStartTime = this.getCurrentTime();
260
- this.packetTimeout = this.txTimePerByte * packetLength + LATENCY_TIMER * 2.0 + 2.0;
261
- }
262
-
263
- setPacketTimeoutMillis(msec) {
264
- this.packetStartTime = this.getCurrentTime();
265
- this.packetTimeout = msec;
266
- }
267
-
268
- isPacketTimeout() {
269
- if (this.getTimeSinceStart() > this.packetTimeout) {
270
- this.packetTimeout = 0;
271
- return true;
272
- }
273
- return false;
274
- }
275
-
276
- getCurrentTime() {
277
- return performance.now();
278
- }
279
-
280
- getTimeSinceStart() {
281
- const timeSince = this.getCurrentTime() - this.packetStartTime;
282
- if (timeSince < 0.0) {
283
- this.packetStartTime = this.getCurrentTime();
284
- }
285
- return timeSince;
286
- }
287
  }
288
 
289
  export class PacketHandler {
290
- constructor(protocolEnd = 0) {
291
- SCS_END = protocolEnd;
292
- debugLog(`PacketHandler initialized with protocol_end=${protocolEnd} (STS/SMS=0, SCS=1)`);
293
- }
294
-
295
- getProtocolVersion() {
296
- return 1.0;
297
- }
298
-
299
- // 获取当前协议端设置的方法
300
- getProtocolEnd() {
301
- return SCS_END;
302
- }
303
-
304
- getTxRxResult(result) {
305
- if (result === COMM_SUCCESS) {
306
- return "[TxRxResult] Communication success!";
307
- } else if (result === COMM_PORT_BUSY) {
308
- return "[TxRxResult] Port is in use!";
309
- } else if (result === COMM_TX_FAIL) {
310
- return "[TxRxResult] Failed transmit instruction packet!";
311
- } else if (result === COMM_RX_FAIL) {
312
- return "[TxRxResult] Failed get status packet from device!";
313
- } else if (result === COMM_TX_ERROR) {
314
- return "[TxRxResult] Incorrect instruction packet!";
315
- } else if (result === COMM_RX_WAITING) {
316
- return "[TxRxResult] Now receiving status packet!";
317
- } else if (result === COMM_RX_TIMEOUT) {
318
- return "[TxRxResult] There is no status packet!";
319
- } else if (result === COMM_RX_CORRUPT) {
320
- return "[TxRxResult] Incorrect status packet!";
321
- } else if (result === COMM_NOT_AVAILABLE) {
322
- return "[TxRxResult] Protocol does not support this function!";
323
- } else {
324
- return "";
325
- }
326
- }
327
-
328
- getRxPacketError(error) {
329
- if (error & ERRBIT_VOLTAGE) {
330
- return "[RxPacketError] Input voltage error!";
331
- }
332
- if (error & ERRBIT_ANGLE) {
333
- return "[RxPacketError] Angle sen error!";
334
- }
335
- if (error & ERRBIT_OVERHEAT) {
336
- return "[RxPacketError] Overheat error!";
337
- }
338
- if (error & ERRBIT_OVERELE) {
339
- return "[RxPacketError] OverEle error!";
340
- }
341
- if (error & ERRBIT_OVERLOAD) {
342
- return "[RxPacketError] Overload error!";
343
- }
344
- return "";
345
- }
346
-
347
- async txPacket(port, txpacket) {
348
- let checksum = 0;
349
- const totalPacketLength = txpacket[PKT_LENGTH] + 4; // 4: HEADER0 HEADER1 ID LENGTH
350
-
351
- if (port.isUsing) {
352
- return COMM_PORT_BUSY;
353
- }
354
- port.isUsing = true;
355
-
356
- // Check max packet length
357
- if (totalPacketLength > TXPACKET_MAX_LEN) {
358
- port.isUsing = false;
359
- return COMM_TX_ERROR;
360
- }
361
-
362
- // Make packet header
363
- txpacket[PKT_HEADER0] = 0xff;
364
- txpacket[PKT_HEADER1] = 0xff;
365
-
366
- // Add checksum to packet
367
- for (let idx = 2; idx < totalPacketLength - 1; idx++) {
368
- checksum += txpacket[idx];
369
- }
370
-
371
- txpacket[totalPacketLength - 1] = ~checksum & 0xff;
372
-
373
- // TX packet
374
- await port.clearPort();
375
- const writtenPacketLength = await port.writePort(txpacket);
376
- if (totalPacketLength !== writtenPacketLength) {
377
- port.isUsing = false;
378
- return COMM_TX_FAIL;
379
- }
380
-
381
- return COMM_SUCCESS;
382
- }
383
-
384
- async rxPacket(port) {
385
- let rxpacket = [];
386
- let result = COMM_RX_FAIL;
387
-
388
- let waitLength = 6; // minimum length (HEADER0 HEADER1 ID LENGTH)
389
-
390
- while (true) {
391
- const data = await port.readPort(waitLength - rxpacket.length);
392
- rxpacket.push(...data);
393
-
394
- if (rxpacket.length >= waitLength) {
395
- // Find packet header
396
- let headerIndex = -1;
397
- for (let i = 0; i < rxpacket.length - 1; i++) {
398
- if (rxpacket[i] === 0xff && rxpacket[i + 1] === 0xff) {
399
- headerIndex = i;
400
- break;
401
- }
402
- }
403
-
404
- if (headerIndex === 0) {
405
- // Found at the beginning of the packet
406
- if (rxpacket[PKT_ID] > 0xfd || rxpacket[PKT_LENGTH] > RXPACKET_MAX_LEN) {
407
- // Invalid ID or length
408
- rxpacket.shift();
409
- continue;
410
- }
411
-
412
- // Recalculate expected packet length
413
- if (waitLength !== rxpacket[PKT_LENGTH] + PKT_LENGTH + 1) {
414
- waitLength = rxpacket[PKT_LENGTH] + PKT_LENGTH + 1;
415
- continue;
416
- }
417
-
418
- if (rxpacket.length < waitLength) {
419
- // Check timeout
420
- if (port.isPacketTimeout()) {
421
- result = rxpacket.length === 0 ? COMM_RX_TIMEOUT : COMM_RX_CORRUPT;
422
- break;
423
- }
424
- continue;
425
- }
426
-
427
- // Calculate checksum
428
- let checksum = 0;
429
- for (let i = 2; i < waitLength - 1; i++) {
430
- checksum += rxpacket[i];
431
- }
432
- checksum = ~checksum & 0xff;
433
-
434
- // Verify checksum
435
- if (rxpacket[waitLength - 1] === checksum) {
436
- result = COMM_SUCCESS;
437
- } else {
438
- result = COMM_RX_CORRUPT;
439
- }
440
- break;
441
- } else if (headerIndex > 0) {
442
- // Remove unnecessary bytes before header
443
- rxpacket = rxpacket.slice(headerIndex);
444
- continue;
445
- }
446
- }
447
-
448
- // Check timeout
449
- if (port.isPacketTimeout()) {
450
- result = rxpacket.length === 0 ? COMM_RX_TIMEOUT : COMM_RX_CORRUPT;
451
- break;
452
- }
453
- }
454
-
455
- if (result !== COMM_SUCCESS) {
456
- debugLog(
457
- `rxPacket result: ${result}, packet: ${rxpacket.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")}`
458
- );
459
- } else {
460
- console.debug(
461
- `rxPacket successful: ${rxpacket.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")}`
462
- );
463
- }
464
- return [rxpacket, result];
465
- }
466
-
467
- async txRxPacket(port, txpacket) {
468
- let rxpacket = null;
469
- let error = 0;
470
- let result = COMM_TX_FAIL;
471
-
472
- try {
473
- // Check if port is already in use
474
- if (port.isUsing) {
475
- debugLog("Port is busy, cannot start new transaction");
476
- return [rxpacket, COMM_PORT_BUSY, error];
477
- }
478
-
479
- // TX packet
480
- debugLog(
481
- "Sending packet:",
482
- txpacket.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")
483
- );
484
-
485
- // Remove retry logic and just send once
486
- result = await this.txPacket(port, txpacket);
487
- debugLog(`TX result: ${result}`);
488
-
489
- if (result !== COMM_SUCCESS) {
490
- debugLog(`TX failed with result: ${result}`);
491
- port.isUsing = false; // Important: Release the port on TX failure
492
- return [rxpacket, result, error];
493
- }
494
-
495
- // If ID is broadcast, no need to wait for status packet
496
- if (txpacket[PKT_ID] === BROADCAST_ID) {
497
- port.isUsing = false;
498
- return [rxpacket, result, error];
499
- }
500
-
501
- // Set packet timeout
502
- if (txpacket[PKT_INSTRUCTION] === INST_READ) {
503
- const length = txpacket[PKT_PARAMETER0 + 1];
504
- // For READ instructions, we expect response to include the data
505
- port.setPacketTimeout(length + 10); // Add extra buffer
506
- debugLog(`Set READ packet timeout for ${length + 10} bytes`);
507
- } else {
508
- // For other instructions, we expect a status packet
509
- port.setPacketTimeout(10); // HEADER0 HEADER1 ID LENGTH ERROR CHECKSUM + buffer
510
- debugLog(`Set standard packet timeout for 10 bytes`);
511
- }
512
-
513
- // RX packet - no retries, just attempt once
514
- debugLog(`Receiving packet`);
515
-
516
- // Clear port before receiving to ensure clean state
517
- await port.clearPort();
518
-
519
- const [rxpacketResult, resultRx] = await this.rxPacket(port);
520
- rxpacket = rxpacketResult;
521
-
522
- // Check if received packet is valid
523
- if (resultRx !== COMM_SUCCESS) {
524
- debugLog(`Rx failed with result: ${resultRx}`);
525
- port.isUsing = false;
526
- return [rxpacket, resultRx, error];
527
- }
528
-
529
- // Verify packet structure
530
- if (rxpacket.length < 6) {
531
- debugLog(`Received packet too short (${rxpacket.length} bytes)`);
532
- port.isUsing = false;
533
- return [rxpacket, COMM_RX_CORRUPT, error];
534
- }
535
-
536
- // Verify packet ID matches the sent ID
537
- if (rxpacket[PKT_ID] !== txpacket[PKT_ID]) {
538
- debugLog(
539
- `Received packet ID (${rxpacket[PKT_ID]}) doesn't match sent ID (${txpacket[PKT_ID]})`
540
- );
541
- port.isUsing = false;
542
- return [rxpacket, COMM_RX_CORRUPT, error];
543
- }
544
-
545
- // Packet looks valid
546
- error = rxpacket[PKT_ERROR];
547
- port.isUsing = false; // Release port on success
548
- return [rxpacket, resultRx, error];
549
- } catch (err) {
550
- console.error("Exception in txRxPacket:", err);
551
- port.isUsing = false; // Release port on exception
552
- return [rxpacket, COMM_RX_FAIL, error];
553
- }
554
- }
555
-
556
- async ping(port, scsId) {
557
- let modelNumber = 0;
558
- let error = 0;
559
-
560
- try {
561
- if (scsId >= BROADCAST_ID) {
562
- debugLog(`Cannot ping broadcast ID ${scsId}`);
563
- return [modelNumber, COMM_NOT_AVAILABLE, error];
564
- }
565
-
566
- const txpacket = new Array(6).fill(0);
567
- txpacket[PKT_ID] = scsId;
568
- txpacket[PKT_LENGTH] = 2;
569
- txpacket[PKT_INSTRUCTION] = INST_PING;
570
-
571
- debugLog(`Pinging servo ID ${scsId}...`);
572
-
573
- // 发送ping指令并获取响应
574
- const [rxpacket, result, err] = await this.txRxPacket(port, txpacket);
575
- error = err;
576
-
577
- // 与Python SDK保持一致:如果ping成功,尝试读取地址3的型号信息
578
- if (result === COMM_SUCCESS) {
579
- debugLog(`Ping to servo ID ${scsId} succeeded, reading model number from address 3`);
580
- // 读取地址3的型号信息(2字节)
581
- const [data, dataResult, dataError] = await this.readTxRx(port, scsId, 3, 2);
582
-
583
- if (dataResult === COMM_SUCCESS && data && data.length >= 2) {
584
- modelNumber = SCS_MAKEWORD(data[0], data[1]);
585
- debugLog(`Model number read: ${modelNumber}`);
586
- } else {
587
- debugLog(`Could not read model number: ${this.getTxRxResult(dataResult)}`);
588
- }
589
- } else {
590
- debugLog(`Ping failed with result: ${result}, error: ${error}`);
591
- }
592
-
593
- return [modelNumber, result, error];
594
- } catch (error) {
595
- console.error(`Exception in ping():`, error);
596
- return [0, COMM_RX_FAIL, 0];
597
- }
598
- }
599
-
600
- // Read methods
601
- async readTxRx(port, scsId, address, length) {
602
- if (scsId >= BROADCAST_ID) {
603
- debugLog("Cannot read from broadcast ID");
604
- return [[], COMM_NOT_AVAILABLE, 0];
605
- }
606
-
607
- // Create read packet
608
- const txpacket = new Array(8).fill(0);
609
- txpacket[PKT_ID] = scsId;
610
- txpacket[PKT_LENGTH] = 4;
611
- txpacket[PKT_INSTRUCTION] = INST_READ;
612
- txpacket[PKT_PARAMETER0] = address;
613
- txpacket[PKT_PARAMETER0 + 1] = length;
614
-
615
- debugLog(`Reading ${length} bytes from address ${address} for servo ID ${scsId}`);
616
-
617
- // Send packet and get response
618
- const [rxpacket, result, error] = await this.txRxPacket(port, txpacket);
619
-
620
- // Process the result
621
- if (result !== COMM_SUCCESS) {
622
- debugLog(`Read failed with result: ${result}, error: ${error}`);
623
- return [[], result, error];
624
- }
625
-
626
- if (!rxpacket || rxpacket.length < PKT_PARAMETER0 + length) {
627
- debugLog(
628
- `Invalid response packet: expected at least ${PKT_PARAMETER0 + length} bytes, got ${rxpacket ? rxpacket.length : 0}`
629
- );
630
- return [[], COMM_RX_CORRUPT, error];
631
- }
632
-
633
- // Extract data from response
634
- const data = [];
635
- debugLog(
636
- `Response packet length: ${rxpacket.length}, extracting ${length} bytes from offset ${PKT_PARAMETER0}`
637
- );
638
- debugLog(
639
- `Response data bytes: ${rxpacket
640
- .slice(PKT_PARAMETER0, PKT_PARAMETER0 + length)
641
- .map((b) => "0x" + b.toString(16).padStart(2, "0"))
642
- .join(" ")}`
643
- );
644
-
645
- for (let i = 0; i < length; i++) {
646
- data.push(rxpacket[PKT_PARAMETER0 + i]);
647
- }
648
-
649
- debugLog(
650
- `Successfully read ${length} bytes: ${data.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")}`
651
- );
652
- return [data, result, error];
653
- }
654
-
655
- async read1ByteTxRx(port, scsId, address) {
656
- const [data, result, error] = await this.readTxRx(port, scsId, address, 1);
657
- const value = data.length > 0 ? data[0] : 0;
658
- return [value, result, error];
659
- }
660
-
661
- async read2ByteTxRx(port, scsId, address) {
662
- const [data, result, error] = await this.readTxRx(port, scsId, address, 2);
663
-
664
- let value = 0;
665
- if (data.length >= 2) {
666
- value = SCS_MAKEWORD(data[0], data[1]);
667
- }
668
-
669
- return [value, result, error];
670
- }
671
-
672
- async read4ByteTxRx(port, scsId, address) {
673
- const [data, result, error] = await this.readTxRx(port, scsId, address, 4);
674
-
675
- let value = 0;
676
- if (data.length >= 4) {
677
- const loword = SCS_MAKEWORD(data[0], data[1]);
678
- const hiword = SCS_MAKEWORD(data[2], data[3]);
679
- value = SCS_MAKEDWORD(loword, hiword);
680
-
681
- debugLog(
682
- `read4ByteTxRx: data=${data.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")}`
683
- );
684
- debugLog(
685
- ` loword=${loword} (0x${loword.toString(16)}), hiword=${hiword} (0x${hiword.toString(16)})`
686
- );
687
- debugLog(` value=${value} (0x${value.toString(16)})`);
688
- }
689
-
690
- return [value, result, error];
691
- }
692
-
693
- // Write methods
694
- async writeTxRx(port, scsId, address, length, data) {
695
- if (scsId >= BROADCAST_ID) {
696
- return [COMM_NOT_AVAILABLE, 0];
697
- }
698
-
699
- // Create write packet
700
- const txpacket = new Array(length + 7).fill(0);
701
- txpacket[PKT_ID] = scsId;
702
- txpacket[PKT_LENGTH] = length + 3;
703
- txpacket[PKT_INSTRUCTION] = INST_WRITE;
704
- txpacket[PKT_PARAMETER0] = address;
705
-
706
- // Add data
707
- for (let i = 0; i < length; i++) {
708
- txpacket[PKT_PARAMETER0 + 1 + i] = data[i] & 0xff;
709
- }
710
-
711
- // Send packet and get response
712
- const [rxpacket, result, error] = await this.txRxPacket(port, txpacket);
713
-
714
- return [result, error];
715
- }
716
-
717
- async write1ByteTxRx(port, scsId, address, data) {
718
- const dataArray = [data & 0xff];
719
- return await this.writeTxRx(port, scsId, address, 1, dataArray);
720
- }
721
-
722
- async write2ByteTxRx(port, scsId, address, data) {
723
- const dataArray = [SCS_LOBYTE(data), SCS_HIBYTE(data)];
724
- return await this.writeTxRx(port, scsId, address, 2, dataArray);
725
- }
726
-
727
- async write4ByteTxRx(port, scsId, address, data) {
728
- const dataArray = [
729
- SCS_LOBYTE(SCS_LOWORD(data)),
730
- SCS_HIBYTE(SCS_LOWORD(data)),
731
- SCS_LOBYTE(SCS_HIWORD(data)),
732
- SCS_HIBYTE(SCS_HIWORD(data))
733
- ];
734
- return await this.writeTxRx(port, scsId, address, 4, dataArray);
735
- }
736
-
737
- // Add syncReadTx for GroupSyncRead functionality
738
- async syncReadTx(port, startAddress, dataLength, param, paramLength) {
739
- // Create packet: HEADER0 HEADER1 ID LEN INST START_ADDR DATA_LEN PARAM... CHKSUM
740
- const txpacket = new Array(paramLength + 8).fill(0);
741
-
742
- txpacket[PKT_ID] = BROADCAST_ID;
743
- txpacket[PKT_LENGTH] = paramLength + 4; // 4: INST START_ADDR DATA_LEN CHKSUM
744
- txpacket[PKT_INSTRUCTION] = INST_SYNC_READ;
745
- txpacket[PKT_PARAMETER0] = startAddress;
746
- txpacket[PKT_PARAMETER0 + 1] = dataLength;
747
-
748
- // Add parameters
749
- for (let i = 0; i < paramLength; i++) {
750
- txpacket[PKT_PARAMETER0 + 2 + i] = param[i];
751
- }
752
-
753
- // Calculate checksum
754
- const totalLen = txpacket[PKT_LENGTH] + 4; // 4: HEADER0 HEADER1 ID LENGTH
755
-
756
- // Add headers
757
- txpacket[PKT_HEADER0] = 0xff;
758
- txpacket[PKT_HEADER1] = 0xff;
759
-
760
- // Calculate checksum
761
- let checksum = 0;
762
- for (let i = 2; i < totalLen - 1; i++) {
763
- checksum += txpacket[i] & 0xff;
764
- }
765
- txpacket[totalLen - 1] = ~checksum & 0xff;
766
-
767
- debugLog(
768
- `SyncReadTx: ${txpacket.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")}`
769
- );
770
-
771
- // Send packet
772
- await port.clearPort();
773
- const bytesWritten = await port.writePort(txpacket);
774
- if (bytesWritten !== totalLen) {
775
- return COMM_TX_FAIL;
776
- }
777
-
778
- // Set timeout based on expected response size
779
- port.setPacketTimeout((6 + dataLength) * paramLength);
780
-
781
- return COMM_SUCCESS;
782
- }
783
-
784
- // Add syncWriteTxOnly for GroupSyncWrite functionality
785
- async syncWriteTxOnly(port, startAddress, dataLength, param, paramLength) {
786
- // Create packet: HEADER0 HEADER1 ID LEN INST START_ADDR DATA_LEN PARAM... CHKSUM
787
- const txpacket = new Array(paramLength + 8).fill(0);
788
-
789
- txpacket[PKT_ID] = BROADCAST_ID;
790
- txpacket[PKT_LENGTH] = paramLength + 4; // 4: INST START_ADDR DATA_LEN CHKSUM
791
- txpacket[PKT_INSTRUCTION] = INST_SYNC_WRITE;
792
- txpacket[PKT_PARAMETER0] = startAddress;
793
- txpacket[PKT_PARAMETER0 + 1] = dataLength;
794
-
795
- // Add parameters
796
- for (let i = 0; i < paramLength; i++) {
797
- txpacket[PKT_PARAMETER0 + 2 + i] = param[i];
798
- }
799
-
800
- // Calculate checksum
801
- const totalLen = txpacket[PKT_LENGTH] + 4; // 4: HEADER0 HEADER1 ID LENGTH
802
-
803
- // Add headers
804
- txpacket[PKT_HEADER0] = 0xff;
805
- txpacket[PKT_HEADER1] = 0xff;
806
-
807
- // Calculate checksum
808
- let checksum = 0;
809
- for (let i = 2; i < totalLen - 1; i++) {
810
- checksum += txpacket[i] & 0xff;
811
- }
812
- txpacket[totalLen - 1] = ~checksum & 0xff;
813
-
814
- debugLog(
815
- `SyncWriteTxOnly: ${txpacket.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")}`
816
- );
817
-
818
- // Send packet - for sync write, we don't need a response
819
- await port.clearPort();
820
- const bytesWritten = await port.writePort(txpacket);
821
- if (bytesWritten !== totalLen) {
822
- return COMM_TX_FAIL;
823
- }
824
-
825
- return COMM_SUCCESS;
826
- }
827
-
828
- // 辅助方法:格式化数据包结构以方便调试
829
- formatPacketStructure(packet) {
830
- if (!packet || packet.length < 4) {
831
- return "Invalid packet (too short)";
832
- }
833
-
834
- try {
835
- let result = "";
836
- result += `HEADER: ${packet[0].toString(16).padStart(2, "0")} ${packet[1].toString(16).padStart(2, "0")} | `;
837
- result += `ID: ${packet[2]} | `;
838
- result += `LENGTH: ${packet[3]} | `;
839
-
840
- if (packet.length >= 5) {
841
- result += `ERROR/INST: ${packet[4].toString(16).padStart(2, "0")} | `;
842
- }
843
-
844
- if (packet.length >= 6) {
845
- result += "PARAMS: ";
846
- for (let i = 5; i < packet.length - 1; i++) {
847
- result += `${packet[i].toString(16).padStart(2, "0")} `;
848
- }
849
- result += `| CHECKSUM: ${packet[packet.length - 1].toString(16).padStart(2, "0")}`;
850
- }
851
-
852
- return result;
853
- } catch (e) {
854
- return "Error formatting packet: " + e.message;
855
- }
856
- }
857
-
858
- /**
859
- * 从响应包中解析舵机型号
860
- * @param {Array} rxpacket - 响应数据包
861
- * @returns {number} 舵机型号
862
- */
863
- parseModelNumber(rxpacket) {
864
- if (!rxpacket || rxpacket.length < 7) {
865
- return 0;
866
- }
867
-
868
- // 检查是否有参数字段
869
- if (rxpacket.length <= PKT_PARAMETER0 + 1) {
870
- return 0;
871
- }
872
-
873
- const param1 = rxpacket[PKT_PARAMETER0];
874
- const param2 = rxpacket[PKT_PARAMETER0 + 1];
875
-
876
- if (SCS_END === 0) {
877
- // STS/SMS 协议的字节顺序
878
- return SCS_MAKEWORD(param1, param2);
879
- } else {
880
- // SCS 协议的字节顺序
881
- return SCS_MAKEWORD(param2, param1);
882
- }
883
- }
884
-
885
- /**
886
- * Verify packet header
887
- * @param {Array} packet - The packet to verify
888
- * @returns {Number} COMM_SUCCESS if packet is valid, error code otherwise
889
- */
890
- getPacketHeader(packet) {
891
- if (!packet || packet.length < 4) {
892
- return COMM_RX_CORRUPT;
893
- }
894
-
895
- // Check header
896
- if (packet[PKT_HEADER0] !== 0xff || packet[PKT_HEADER1] !== 0xff) {
897
- return COMM_RX_CORRUPT;
898
- }
899
-
900
- // Check ID validity
901
- if (packet[PKT_ID] > 0xfd) {
902
- return COMM_RX_CORRUPT;
903
- }
904
-
905
- // Check length
906
- if (packet.length != packet[PKT_LENGTH] + 4) {
907
- return COMM_RX_CORRUPT;
908
- }
909
-
910
- // Calculate checksum
911
- let checksum = 0;
912
- for (let i = 2; i < packet.length - 1; i++) {
913
- checksum += packet[i] & 0xff;
914
- }
915
- checksum = ~checksum & 0xff;
916
-
917
- // Verify checksum
918
- if (packet[packet.length - 1] !== checksum) {
919
- return COMM_RX_CORRUPT;
920
- }
921
-
922
- return COMM_SUCCESS;
923
- }
924
  }
925
 
926
  /**
@@ -928,213 +901,200 @@ export class PacketHandler {
928
  * - This class is used to read multiple servos with the same control table address at once
929
  */
930
  export class GroupSyncRead {
931
- constructor(port, ph, startAddress, dataLength) {
932
- this.port = port;
933
- this.ph = ph;
934
- this.startAddress = startAddress;
935
- this.dataLength = dataLength;
936
-
937
- this.isAvailableServiceID = new Set();
938
- this.dataDict = new Map();
939
- this.param = [];
940
- this.clearParam();
941
- }
942
-
943
- makeParam() {
944
- this.param = [];
945
- for (const id of this.isAvailableServiceID) {
946
- this.param.push(id);
947
- }
948
- return this.param.length;
949
- }
950
-
951
- addParam(scsId) {
952
- if (this.isAvailableServiceID.has(scsId)) {
953
- return false;
954
- }
955
-
956
- this.isAvailableServiceID.add(scsId);
957
- this.dataDict.set(scsId, new Array(this.dataLength).fill(0));
958
- return true;
959
- }
960
-
961
- removeParam(scsId) {
962
- if (!this.isAvailableServiceID.has(scsId)) {
963
- return false;
964
- }
965
-
966
- this.isAvailableServiceID.delete(scsId);
967
- this.dataDict.delete(scsId);
968
- return true;
969
- }
970
-
971
- clearParam() {
972
- this.isAvailableServiceID.clear();
973
- this.dataDict.clear();
974
- return true;
975
- }
976
-
977
- async txPacket() {
978
- if (this.isAvailableServiceID.size === 0) {
979
- return COMM_NOT_AVAILABLE;
980
- }
981
-
982
- const paramLength = this.makeParam();
983
- return await this.ph.syncReadTx(
984
- this.port,
985
- this.startAddress,
986
- this.dataLength,
987
- this.param,
988
- paramLength
989
- );
990
- }
991
-
992
- async rxPacket() {
993
- let result = COMM_RX_FAIL;
994
-
995
- if (this.isAvailableServiceID.size === 0) {
996
- return COMM_NOT_AVAILABLE;
997
- }
998
-
999
- // Set all servos' data as invalid
1000
- for (const id of this.isAvailableServiceID) {
1001
- this.dataDict.set(id, new Array(this.dataLength).fill(0));
1002
- }
1003
-
1004
- const [rxpacket, rxResult] = await this.ph.rxPacket(this.port);
1005
- if (rxResult !== COMM_SUCCESS || !rxpacket || rxpacket.length === 0) {
1006
- return rxResult;
1007
- }
1008
-
1009
- // More tolerant of packets with unexpected values in the PKT_ERROR field
1010
- // Don't require INST_STATUS to be exactly 0x55
1011
- debugLog(
1012
- `GroupSyncRead rxPacket: ID=${rxpacket[PKT_ID]}, ERROR/INST field=0x${rxpacket[PKT_ERROR].toString(16)}`
1013
- );
1014
-
1015
- // Check if the packet matches any of the available IDs
1016
- if (!this.isAvailableServiceID.has(rxpacket[PKT_ID])) {
1017
- debugLog(
1018
- `Received packet with ID ${rxpacket[PKT_ID]} which is not in the available IDs list`
1019
- );
1020
- return COMM_RX_CORRUPT;
1021
- }
1022
-
1023
- // Extract data for the matching ID
1024
- const scsId = rxpacket[PKT_ID];
1025
- const data = new Array(this.dataLength).fill(0);
1026
-
1027
- // Extract the parameter data, which should start at PKT_PARAMETER0
1028
- if (rxpacket.length < PKT_PARAMETER0 + this.dataLength) {
1029
- debugLog(
1030
- `Packet too short: expected ${PKT_PARAMETER0 + this.dataLength} bytes, got ${rxpacket.length}`
1031
- );
1032
- return COMM_RX_CORRUPT;
1033
- }
1034
-
1035
- for (let i = 0; i < this.dataLength; i++) {
1036
- data[i] = rxpacket[PKT_PARAMETER0 + i];
1037
- }
1038
-
1039
- // Update the data dict
1040
- this.dataDict.set(scsId, data);
1041
- debugLog(
1042
- `Updated data for servo ID ${scsId}: ${data.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")}`
1043
- );
1044
-
1045
- // Continue receiving until timeout or all data is received
1046
- if (this.isAvailableServiceID.size > 1) {
1047
- result = await this.rxPacket();
1048
- } else {
1049
- result = COMM_SUCCESS;
1050
- }
1051
-
1052
- return result;
1053
- }
1054
-
1055
- async txRxPacket() {
1056
- try {
1057
- // First check if port is being used
1058
- if (this.port.isUsing) {
1059
- debugLog("Port is busy, cannot start sync read operation");
1060
- return COMM_PORT_BUSY;
1061
- }
1062
-
1063
- // Start the transmission
1064
- debugLog("Starting sync read TX/RX operation...");
1065
- let result = await this.txPacket();
1066
- if (result !== COMM_SUCCESS) {
1067
- debugLog(`Sync read TX failed with result: ${result}`);
1068
- return result;
1069
- }
1070
-
1071
- // Get a single response with a standard timeout
1072
- debugLog(`Attempting to receive a response...`);
1073
-
1074
- // Receive a single response
1075
- result = await this.rxPacket();
1076
-
1077
- // Release port
1078
- this.port.isUsing = false;
1079
-
1080
- return result;
1081
- } catch (error) {
1082
- console.error("Exception in GroupSyncRead txRxPacket:", error);
1083
- // Make sure port is released
1084
- this.port.isUsing = false;
1085
- return COMM_RX_FAIL;
1086
- }
1087
- }
1088
-
1089
- isAvailable(scsId, address, dataLength) {
1090
- if (!this.isAvailableServiceID.has(scsId)) {
1091
- return false;
1092
- }
1093
-
1094
- const startAddr = this.startAddress;
1095
- const endAddr = startAddr + this.dataLength - 1;
1096
-
1097
- const reqStartAddr = address;
1098
- const reqEndAddr = reqStartAddr + dataLength - 1;
1099
-
1100
- if (reqStartAddr < startAddr || reqEndAddr > endAddr) {
1101
- return false;
1102
- }
1103
-
1104
- const data = this.dataDict.get(scsId);
1105
- if (!data || data.length === 0) {
1106
- return false;
1107
- }
1108
-
1109
- return true;
1110
- }
1111
-
1112
- getData(scsId, address, dataLength) {
1113
- if (!this.isAvailable(scsId, address, dataLength)) {
1114
- return 0;
1115
- }
1116
-
1117
- const startAddr = this.startAddress;
1118
- const data = this.dataDict.get(scsId);
1119
-
1120
- // Calculate data offset
1121
- const dataOffset = address - startAddr;
1122
-
1123
- // Combine bytes according to dataLength
1124
- switch (dataLength) {
1125
- case 1:
1126
- return data[dataOffset];
1127
- case 2:
1128
- return SCS_MAKEWORD(data[dataOffset], data[dataOffset + 1]);
1129
- case 4:
1130
- return SCS_MAKEDWORD(
1131
- SCS_MAKEWORD(data[dataOffset], data[dataOffset + 1]),
1132
- SCS_MAKEWORD(data[dataOffset + 2], data[dataOffset + 3])
1133
- );
1134
- default:
1135
- return 0;
1136
- }
1137
- }
1138
  }
1139
 
1140
  /**
@@ -1142,94 +1102,84 @@ export class GroupSyncRead {
1142
  * - This class is used to write multiple servos with the same control table address at once
1143
  */
1144
  export class GroupSyncWrite {
1145
- constructor(port, ph, startAddress, dataLength) {
1146
- this.port = port;
1147
- this.ph = ph;
1148
- this.startAddress = startAddress;
1149
- this.dataLength = dataLength;
1150
-
1151
- this.isAvailableServiceID = new Set();
1152
- this.dataDict = new Map();
1153
- this.param = [];
1154
- this.clearParam();
1155
- }
1156
-
1157
- makeParam() {
1158
- this.param = [];
1159
- for (const id of this.isAvailableServiceID) {
1160
- // Add ID to parameter
1161
- this.param.push(id);
1162
-
1163
- // Add data to parameter
1164
- const data = this.dataDict.get(id);
1165
- for (let i = 0; i < this.dataLength; i++) {
1166
- this.param.push(data[i]);
1167
- }
1168
- }
1169
- return this.param.length;
1170
- }
1171
-
1172
- addParam(scsId, data) {
1173
- if (this.isAvailableServiceID.has(scsId)) {
1174
- return false;
1175
- }
1176
-
1177
- if (data.length !== this.dataLength) {
1178
- console.error(
1179
- `Data length (${data.length}) doesn't match required length (${this.dataLength})`
1180
- );
1181
- return false;
1182
- }
1183
-
1184
- this.isAvailableServiceID.add(scsId);
1185
- this.dataDict.set(scsId, data);
1186
- return true;
1187
- }
1188
-
1189
- removeParam(scsId) {
1190
- if (!this.isAvailableServiceID.has(scsId)) {
1191
- return false;
1192
- }
1193
-
1194
- this.isAvailableServiceID.delete(scsId);
1195
- this.dataDict.delete(scsId);
1196
- return true;
1197
- }
1198
-
1199
- changeParam(scsId, data) {
1200
- if (!this.isAvailableServiceID.has(scsId)) {
1201
- return false;
1202
- }
1203
-
1204
- if (data.length !== this.dataLength) {
1205
- console.error(
1206
- `Data length (${data.length}) doesn't match required length (${this.dataLength})`
1207
- );
1208
- return false;
1209
- }
1210
-
1211
- this.dataDict.set(scsId, data);
1212
- return true;
1213
- }
1214
-
1215
- clearParam() {
1216
- this.isAvailableServiceID.clear();
1217
- this.dataDict.clear();
1218
- return true;
1219
- }
1220
-
1221
- async txPacket() {
1222
- if (this.isAvailableServiceID.size === 0) {
1223
- return COMM_NOT_AVAILABLE;
1224
- }
1225
-
1226
- const paramLength = this.makeParam();
1227
- return await this.ph.syncWriteTxOnly(
1228
- this.port,
1229
- this.startAddress,
1230
- this.dataLength,
1231
- this.param,
1232
- paramLength
1233
- );
1234
- }
1235
  }
 
 
 
 
1
  // Constants
2
+ export const BROADCAST_ID = 0xFE; // 254
3
+ export const MAX_ID = 0xFC; // 252
4
 
5
  // Protocol instructions
6
  export const INST_PING = 1;
 
8
  export const INST_WRITE = 3;
9
  export const INST_REG_WRITE = 4;
10
  export const INST_ACTION = 5;
11
+ export const INST_SYNC_WRITE = 131; // 0x83
12
+ export const INST_SYNC_READ = 130; // 0x82
13
+ export const INST_STATUS = 85; // 0x55, status packet instruction (0x55)
14
 
15
  // Communication results
16
+ export const COMM_SUCCESS = 0; // tx or rx packet communication success
17
+ export const COMM_PORT_BUSY = -1; // Port is busy (in use)
18
+ export const COMM_TX_FAIL = -2; // Failed transmit instruction packet
19
+ export const COMM_RX_FAIL = -3; // Failed get status packet
20
+ export const COMM_TX_ERROR = -4; // Incorrect instruction packet
21
+ export const COMM_RX_WAITING = -5; // Now receiving status packet
22
+ export const COMM_RX_TIMEOUT = -6; // There is no status packet
23
+ export const COMM_RX_CORRUPT = -7; // Incorrect status packet
24
  export const COMM_NOT_AVAILABLE = -9;
25
 
26
  // Packet constants
 
52
 
53
  // Utility functions for handling word operations
54
  export function SCS_LOWORD(l) {
55
+ return l & 0xFFFF;
56
  }
57
 
58
  export function SCS_HIWORD(l) {
59
+ return (l >> 16) & 0xFFFF;
60
  }
61
 
62
  export function SCS_LOBYTE(w) {
63
+ if (SCS_END === 0) {
64
+ return w & 0xFF;
65
+ } else {
66
+ return (w >> 8) & 0xFF;
67
+ }
68
  }
69
 
70
  export function SCS_HIBYTE(w) {
71
+ if (SCS_END === 0) {
72
+ return (w >> 8) & 0xFF;
73
+ } else {
74
+ return w & 0xFF;
75
+ }
76
  }
77
 
78
  export function SCS_MAKEWORD(a, b) {
79
+ if (SCS_END === 0) {
80
+ return (a & 0xFF) | ((b & 0xFF) << 8);
81
+ } else {
82
+ return (b & 0xFF) | ((a & 0xFF) << 8);
83
+ }
84
  }
85
 
86
  export function SCS_MAKEDWORD(a, b) {
87
+ return (a & 0xFFFF) | ((b & 0xFFFF) << 16);
88
  }
89
 
90
  export function SCS_TOHOST(a, b) {
91
+ if (a & (1 << b)) {
92
+ return -(a & ~(1 << b));
93
+ } else {
94
+ return a;
95
+ }
96
  }
97
 
98
  export class PortHandler {
99
+ constructor() {
100
+ this.port = null;
101
+ this.reader = null;
102
+ this.writer = null;
103
+ this.isOpen = false;
104
+ this.isUsing = false;
105
+ this.baudrate = DEFAULT_BAUDRATE;
106
+ this.packetStartTime = 0;
107
+ this.packetTimeout = 0;
108
+ this.txTimePerByte = 0;
109
+ }
110
+
111
+ async requestPort() {
112
+ try {
113
+ this.port = await navigator.serial.requestPort();
114
+ return true;
115
+ } catch (err) {
116
+ console.error('Error requesting serial port:', err);
117
+ return false;
118
+ }
119
+ }
120
+
121
+ async openPort() {
122
+ if (!this.port) {
123
+ return false;
124
+ }
125
+
126
+ try {
127
+ await this.port.open({ baudRate: this.baudrate });
128
+ this.reader = this.port.readable.getReader();
129
+ this.writer = this.port.writable.getWriter();
130
+ this.isOpen = true;
131
+ this.txTimePerByte = (1000.0 / this.baudrate) * 10.0;
132
+ return true;
133
+ } catch (err) {
134
+ console.error('Error opening port:', err);
135
+ return false;
136
+ }
137
+ }
138
+
139
+ async closePort() {
140
+ if (this.reader) {
141
+ await this.reader.releaseLock();
142
+ this.reader = null;
143
+ }
144
+
145
+ if (this.writer) {
146
+ await this.writer.releaseLock();
147
+ this.writer = null;
148
+ }
149
+
150
+ if (this.port && this.isOpen) {
151
+ await this.port.close();
152
+ this.isOpen = false;
153
+ }
154
+ }
155
+
156
+ async clearPort() {
157
+ if (this.reader) {
158
+ await this.reader.releaseLock();
159
+ this.reader = this.port.readable.getReader();
160
+ }
161
+ }
162
+
163
+ setBaudRate(baudrate) {
164
+ this.baudrate = baudrate;
165
+ this.txTimePerByte = (1000.0 / this.baudrate) * 10.0;
166
+ return true;
167
+ }
168
+
169
+ getBaudRate() {
170
+ return this.baudrate;
171
+ }
172
+
173
+ async writePort(data) {
174
+ if (!this.isOpen || !this.writer) {
175
+ return 0;
176
+ }
177
+
178
+ try {
179
+ await this.writer.write(new Uint8Array(data));
180
+ return data.length;
181
+ } catch (err) {
182
+ console.error('Error writing to port:', err);
183
+ return 0;
184
+ }
185
+ }
186
+
187
+ async readPort(length) {
188
+ if (!this.isOpen || !this.reader) {
189
+ return [];
190
+ }
191
+
192
+ try {
193
+ // Increase timeout for more reliable data reception
194
+ const timeoutMs = 500;
195
+ let totalBytes = [];
196
+ const startTime = performance.now();
197
+
198
+ // Continue reading until we get enough bytes or timeout
199
+ while (totalBytes.length < length) {
200
+ // Create a timeout promise
201
+ const timeoutPromise = new Promise(resolve => {
202
+ setTimeout(() => resolve({ value: new Uint8Array(), done: false, timeout: true }), 100); // Short internal timeout
203
+ });
204
+
205
+ // Race between reading and timeout
206
+ const result = await Promise.race([
207
+ this.reader.read(),
208
+ timeoutPromise
209
+ ]);
210
+
211
+ if (result.timeout) {
212
+ // Internal timeout - check if we've exceeded total timeout
213
+ if (performance.now() - startTime > timeoutMs) {
214
+ console.log(`readPort total timeout after ${timeoutMs}ms`);
215
+ break;
216
+ }
217
+ continue; // Try reading again
218
+ }
219
+
220
+ if (result.done) {
221
+ console.log('Reader done, stream closed');
222
+ break;
223
+ }
224
+
225
+ if (result.value.length === 0) {
226
+ // If there's no data but we haven't timed out yet, wait briefly and try again
227
+ await new Promise(resolve => setTimeout(resolve, 10));
228
+
229
+ // Check if we've exceeded total timeout
230
+ if (performance.now() - startTime > timeoutMs) {
231
+ console.log(`readPort total timeout after ${timeoutMs}ms`);
232
+ break;
233
+ }
234
+ continue;
235
+ }
236
+
237
+ // Add received bytes to our total
238
+ const newData = Array.from(result.value);
239
+ totalBytes.push(...newData);
240
+ // console.log(`Read ${newData.length} bytes:`, newData.map(b => b.toString(16).padStart(2, '0')).join(' '));
241
+
242
+ // If we've got enough data, we can stop
243
+ if (totalBytes.length >= length) {
244
+ break;
245
+ }
246
+ }
247
+
248
+ return totalBytes;
249
+ } catch (err) {
250
+ console.error('Error reading from port:', err);
251
+ return [];
252
+ }
253
+ }
254
+
255
+ setPacketTimeout(packetLength) {
256
+ this.packetStartTime = this.getCurrentTime();
257
+ this.packetTimeout = (this.txTimePerByte * packetLength) + (LATENCY_TIMER * 2.0) + 2.0;
258
+ }
259
+
260
+ setPacketTimeoutMillis(msec) {
261
+ this.packetStartTime = this.getCurrentTime();
262
+ this.packetTimeout = msec;
263
+ }
264
+
265
+ isPacketTimeout() {
266
+ if (this.getTimeSinceStart() > this.packetTimeout) {
267
+ this.packetTimeout = 0;
268
+ return true;
269
+ }
270
+ return false;
271
+ }
272
+
273
+ getCurrentTime() {
274
+ return performance.now();
275
+ }
276
+
277
+ getTimeSinceStart() {
278
+ const timeSince = this.getCurrentTime() - this.packetStartTime;
279
+ if (timeSince < 0.0) {
280
+ this.packetStartTime = this.getCurrentTime();
281
+ }
282
+ return timeSince;
283
+ }
284
  }
285
 
286
  export class PacketHandler {
287
+ constructor(protocolEnd = 0) {
288
+ SCS_END = protocolEnd;
289
+ console.log(`PacketHandler initialized with protocol_end=${protocolEnd} (STS/SMS=0, SCS=1)`);
290
+ }
291
+
292
+ getProtocolVersion() {
293
+ return 1.0;
294
+ }
295
+
296
+ // 获取当前协议端设置的方法
297
+ getProtocolEnd() {
298
+ return SCS_END;
299
+ }
300
+
301
+ getTxRxResult(result) {
302
+ if (result === COMM_SUCCESS) {
303
+ return "[TxRxResult] Communication success!";
304
+ } else if (result === COMM_PORT_BUSY) {
305
+ return "[TxRxResult] Port is in use!";
306
+ } else if (result === COMM_TX_FAIL) {
307
+ return "[TxRxResult] Failed transmit instruction packet!";
308
+ } else if (result === COMM_RX_FAIL) {
309
+ return "[TxRxResult] Failed get status packet from device!";
310
+ } else if (result === COMM_TX_ERROR) {
311
+ return "[TxRxResult] Incorrect instruction packet!";
312
+ } else if (result === COMM_RX_WAITING) {
313
+ return "[TxRxResult] Now receiving status packet!";
314
+ } else if (result === COMM_RX_TIMEOUT) {
315
+ return "[TxRxResult] There is no status packet!";
316
+ } else if (result === COMM_RX_CORRUPT) {
317
+ return "[TxRxResult] Incorrect status packet!";
318
+ } else if (result === COMM_NOT_AVAILABLE) {
319
+ return "[TxRxResult] Protocol does not support this function!";
320
+ } else {
321
+ return "";
322
+ }
323
+ }
324
+
325
+ getRxPacketError(error) {
326
+ if (error & ERRBIT_VOLTAGE) {
327
+ return "[RxPacketError] Input voltage error!";
328
+ }
329
+ if (error & ERRBIT_ANGLE) {
330
+ return "[RxPacketError] Angle sen error!";
331
+ }
332
+ if (error & ERRBIT_OVERHEAT) {
333
+ return "[RxPacketError] Overheat error!";
334
+ }
335
+ if (error & ERRBIT_OVERELE) {
336
+ return "[RxPacketError] OverEle error!";
337
+ }
338
+ if (error & ERRBIT_OVERLOAD) {
339
+ return "[RxPacketError] Overload error!";
340
+ }
341
+ return "";
342
+ }
343
+
344
+ async txPacket(port, txpacket) {
345
+ let checksum = 0;
346
+ const totalPacketLength = txpacket[PKT_LENGTH] + 4; // 4: HEADER0 HEADER1 ID LENGTH
347
+
348
+ if (port.isUsing) {
349
+ return COMM_PORT_BUSY;
350
+ }
351
+ port.isUsing = true;
352
+
353
+ // Check max packet length
354
+ if (totalPacketLength > TXPACKET_MAX_LEN) {
355
+ port.isUsing = false;
356
+ return COMM_TX_ERROR;
357
+ }
358
+
359
+ // Make packet header
360
+ txpacket[PKT_HEADER0] = 0xFF;
361
+ txpacket[PKT_HEADER1] = 0xFF;
362
+
363
+ // Add checksum to packet
364
+ for (let idx = 2; idx < totalPacketLength - 1; idx++) {
365
+ checksum += txpacket[idx];
366
+ }
367
+
368
+ txpacket[totalPacketLength - 1] = (~checksum) & 0xFF;
369
+
370
+ // TX packet
371
+ await port.clearPort();
372
+ const writtenPacketLength = await port.writePort(txpacket);
373
+ if (totalPacketLength !== writtenPacketLength) {
374
+ port.isUsing = false;
375
+ return COMM_TX_FAIL;
376
+ }
377
+
378
+ return COMM_SUCCESS;
379
+ }
380
+
381
+ async rxPacket(port) {
382
+ let rxpacket = [];
383
+ let result = COMM_RX_FAIL;
384
+
385
+ let waitLength = 6; // minimum length (HEADER0 HEADER1 ID LENGTH)
386
+
387
+ while (true) {
388
+ const data = await port.readPort(waitLength - rxpacket.length);
389
+ rxpacket.push(...data);
390
+
391
+ if (rxpacket.length >= waitLength) {
392
+ // Find packet header
393
+ let headerIndex = -1;
394
+ for (let i = 0; i < rxpacket.length - 1; i++) {
395
+ if (rxpacket[i] === 0xFF && rxpacket[i + 1] === 0xFF) {
396
+ headerIndex = i;
397
+ break;
398
+ }
399
+ }
400
+
401
+ if (headerIndex === 0) {
402
+ // Found at the beginning of the packet
403
+ if (rxpacket[PKT_ID] > 0xFD || rxpacket[PKT_LENGTH] > RXPACKET_MAX_LEN) {
404
+ // Invalid ID or length
405
+ rxpacket.shift();
406
+ continue;
407
+ }
408
+
409
+ // Recalculate expected packet length
410
+ if (waitLength !== (rxpacket[PKT_LENGTH] + PKT_LENGTH + 1)) {
411
+ waitLength = rxpacket[PKT_LENGTH] + PKT_LENGTH + 1;
412
+ continue;
413
+ }
414
+
415
+ if (rxpacket.length < waitLength) {
416
+ // Check timeout
417
+ if (port.isPacketTimeout()) {
418
+ result = rxpacket.length === 0 ? COMM_RX_TIMEOUT : COMM_RX_CORRUPT;
419
+ break;
420
+ }
421
+ continue;
422
+ }
423
+
424
+ // Calculate checksum
425
+ let checksum = 0;
426
+ for (let i = 2; i < waitLength - 1; i++) {
427
+ checksum += rxpacket[i];
428
+ }
429
+ checksum = (~checksum) & 0xFF;
430
+
431
+ // Verify checksum
432
+ if (rxpacket[waitLength - 1] === checksum) {
433
+ result = COMM_SUCCESS;
434
+ } else {
435
+ result = COMM_RX_CORRUPT;
436
+ }
437
+ break;
438
+ } else if (headerIndex > 0) {
439
+ // Remove unnecessary bytes before header
440
+ rxpacket = rxpacket.slice(headerIndex);
441
+ continue;
442
+ }
443
+ }
444
+
445
+ // Check timeout
446
+ if (port.isPacketTimeout()) {
447
+ result = rxpacket.length === 0 ? COMM_RX_TIMEOUT : COMM_RX_CORRUPT;
448
+ break;
449
+ }
450
+ }
451
+
452
+ if (result !== COMM_SUCCESS) {
453
+ console.log(`rxPacket result: ${result}, packet: ${rxpacket.map(b => '0x' + b.toString(16).padStart(2,'0')).join(' ')}`);
454
+ } else {
455
+ // console.debug(`rxPacket successful: ${rxpacket.map(b => '0x' + b.toString(16).padStart(2,'0')).join(' ')}`);
456
+ }
457
+ return [rxpacket, result];
458
+ }
459
+
460
+ async txRxPacket(port, txpacket) {
461
+ let rxpacket = null;
462
+ let error = 0;
463
+ let result = COMM_TX_FAIL;
464
+
465
+ try {
466
+ // Check if port is already in use
467
+ if (port.isUsing) {
468
+ console.log("Port is busy, cannot start new transaction");
469
+ return [rxpacket, COMM_PORT_BUSY, error];
470
+ }
471
+
472
+ // TX packet
473
+ // console.log("Sending packet:", txpacket.map(b => '0x' + b.toString(16).padStart(2,'0')).join(' '));
474
+
475
+ // Remove retry logic and just send once
476
+ result = await this.txPacket(port, txpacket);
477
+ // console.log(`TX result: ${result}`);
478
+
479
+ if (result !== COMM_SUCCESS) {
480
+ console.log(`TX failed with result: ${result}`);
481
+ port.isUsing = false; // Important: Release the port on TX failure
482
+ return [rxpacket, result, error];
483
+ }
484
+
485
+ // If ID is broadcast, no need to wait for status packet
486
+ if (txpacket[PKT_ID] === BROADCAST_ID) {
487
+ port.isUsing = false;
488
+ return [rxpacket, result, error];
489
+ }
490
+
491
+ // Set packet timeout
492
+ if (txpacket[PKT_INSTRUCTION] === INST_READ) {
493
+ const length = txpacket[PKT_PARAMETER0 + 1];
494
+ // For READ instructions, we expect response to include the data
495
+ port.setPacketTimeout(length + 10); // Add extra buffer
496
+ // console.log(`Set READ packet timeout for ${length + 10} bytes`);
497
+ } else {
498
+ // For other instructions, we expect a status packet
499
+ port.setPacketTimeout(10); // HEADER0 HEADER1 ID LENGTH ERROR CHECKSUM + buffer
500
+ console.log(`Set standard packet timeout for 10 bytes`);
501
+ }
502
+
503
+ // RX packet - no retries, just attempt once
504
+ // console.log(`Receiving packet`);
505
+
506
+ // Clear port before receiving to ensure clean state
507
+ await port.clearPort();
508
+
509
+ const [rxpacketResult, resultRx] = await this.rxPacket(port);
510
+ rxpacket = rxpacketResult;
511
+
512
+ // Check if received packet is valid
513
+ if (resultRx !== COMM_SUCCESS) {
514
+ console.log(`Rx failed with result: ${resultRx}`);
515
+ port.isUsing = false;
516
+ return [rxpacket, resultRx, error];
517
+ }
518
+
519
+ // Verify packet structure
520
+ if (rxpacket.length < 6) {
521
+ console.log(`Received packet too short (${rxpacket.length} bytes)`);
522
+ port.isUsing = false;
523
+ return [rxpacket, COMM_RX_CORRUPT, error];
524
+ }
525
+
526
+ // Verify packet ID matches the sent ID
527
+ if (rxpacket[PKT_ID] !== txpacket[PKT_ID]) {
528
+ console.log(`Received packet ID (${rxpacket[PKT_ID]}) doesn't match sent ID (${txpacket[PKT_ID]})`);
529
+ port.isUsing = false;
530
+ return [rxpacket, COMM_RX_CORRUPT, error];
531
+ }
532
+
533
+ // Packet looks valid
534
+ error = rxpacket[PKT_ERROR];
535
+ port.isUsing = false; // Release port on success
536
+ return [rxpacket, resultRx, error];
537
+
538
+ } catch (err) {
539
+ console.error("Exception in txRxPacket:", err);
540
+ port.isUsing = false; // Release port on exception
541
+ return [rxpacket, COMM_RX_FAIL, error];
542
+ }
543
+ }
544
+
545
+ async ping(port, scsId) {
546
+ let modelNumber = 0;
547
+ let error = 0;
548
+
549
+ try {
550
+ if (scsId >= BROADCAST_ID) {
551
+ console.log(`Cannot ping broadcast ID ${scsId}`);
552
+ return [modelNumber, COMM_NOT_AVAILABLE, error];
553
+ }
554
+
555
+ const txpacket = new Array(6).fill(0);
556
+ txpacket[PKT_ID] = scsId;
557
+ txpacket[PKT_LENGTH] = 2;
558
+ txpacket[PKT_INSTRUCTION] = INST_PING;
559
+
560
+ console.log(`Pinging servo ID ${scsId}...`);
561
+
562
+ // 发送ping指令并获取响应
563
+ const [rxpacket, result, err] = await this.txRxPacket(port, txpacket);
564
+ error = err;
565
+
566
+ // 与Python SDK保持一致:如果ping成功,尝试读取地址3的型号信息
567
+ if (result === COMM_SUCCESS) {
568
+ console.log(`Ping to servo ID ${scsId} succeeded, reading model number from address 3`);
569
+ // 读取地址3的型号信息(2字节)
570
+ const [data, dataResult, dataError] = await this.readTxRx(port, scsId, 3, 2);
571
+
572
+ if (dataResult === COMM_SUCCESS && data && data.length >= 2) {
573
+ modelNumber = SCS_MAKEWORD(data[0], data[1]);
574
+ console.log(`Model number read: ${modelNumber}`);
575
+ } else {
576
+ console.log(`Could not read model number: ${this.getTxRxResult(dataResult)}`);
577
+ }
578
+ } else {
579
+ console.log(`Ping failed with result: ${result}, error: ${error}`);
580
+ }
581
+
582
+ return [modelNumber, result, error];
583
+ } catch (error) {
584
+ console.error(`Exception in ping():`, error);
585
+ return [0, COMM_RX_FAIL, 0];
586
+ }
587
+ }
588
+
589
+ // Read methods
590
+ async readTxRx(port, scsId, address, length) {
591
+ if (scsId >= BROADCAST_ID) {
592
+ console.log('Cannot read from broadcast ID');
593
+ return [[], COMM_NOT_AVAILABLE, 0];
594
+ }
595
+
596
+ // Create read packet
597
+ const txpacket = new Array(8).fill(0);
598
+ txpacket[PKT_ID] = scsId;
599
+ txpacket[PKT_LENGTH] = 4;
600
+ txpacket[PKT_INSTRUCTION] = INST_READ;
601
+ txpacket[PKT_PARAMETER0] = address;
602
+ txpacket[PKT_PARAMETER0 + 1] = length;
603
+
604
+ // console.log(`Reading ${length} bytes from address ${address} for servo ID ${scsId}`);
605
+
606
+ // Send packet and get response
607
+ const [rxpacket, result, error] = await this.txRxPacket(port, txpacket);
608
+
609
+ // Process the result
610
+ if (result !== COMM_SUCCESS) {
611
+ console.log(`Read failed with result: ${result}, error: ${error}`);
612
+ return [[], result, error];
613
+ }
614
+
615
+ if (!rxpacket || rxpacket.length < PKT_PARAMETER0 + length) {
616
+ console.log(`Invalid response packet: expected at least ${PKT_PARAMETER0 + length} bytes, got ${rxpacket ? rxpacket.length : 0}`);
617
+ return [[], COMM_RX_CORRUPT, error];
618
+ }
619
+
620
+ // Extract data from response
621
+ const data = [];
622
+ // console.log(`Response packet length: ${rxpacket.length}, extracting ${length} bytes from offset ${PKT_PARAMETER0}`);
623
+ // console.log(`Response data bytes: ${rxpacket.slice(PKT_PARAMETER0, PKT_PARAMETER0 + length).map(b => '0x' + b.toString(16).padStart(2,'0')).join(' ')}`);
624
+
625
+ for (let i = 0; i < length; i++) {
626
+ data.push(rxpacket[PKT_PARAMETER0 + i]);
627
+ }
628
+
629
+ // console.log(`Successfully read ${length} bytes: ${data.map(b => '0x' + b.toString(16).padStart(2,'0')).join(' ')}`);
630
+ return [data, result, error];
631
+ }
632
+
633
+ async read1ByteTxRx(port, scsId, address) {
634
+ const [data, result, error] = await this.readTxRx(port, scsId, address, 1);
635
+ const value = (data.length > 0) ? data[0] : 0;
636
+ return [value, result, error];
637
+ }
638
+
639
+ async read2ByteTxRx(port, scsId, address) {
640
+ const [data, result, error] = await this.readTxRx(port, scsId, address, 2);
641
+
642
+ let value = 0;
643
+ if (data.length >= 2) {
644
+ value = SCS_MAKEWORD(data[0], data[1]);
645
+ }
646
+
647
+ return [value, result, error];
648
+ }
649
+
650
+ async read4ByteTxRx(port, scsId, address) {
651
+ const [data, result, error] = await this.readTxRx(port, scsId, address, 4);
652
+
653
+ let value = 0;
654
+ if (data.length >= 4) {
655
+ const loword = SCS_MAKEWORD(data[0], data[1]);
656
+ const hiword = SCS_MAKEWORD(data[2], data[3]);
657
+ value = SCS_MAKEDWORD(loword, hiword);
658
+
659
+ console.log(`read4ByteTxRx: data=${data.map(b => '0x' + b.toString(16).padStart(2,'0')).join(' ')}`);
660
+ console.log(` loword=${loword} (0x${loword.toString(16)}), hiword=${hiword} (0x${hiword.toString(16)})`);
661
+ console.log(` value=${value} (0x${value.toString(16)})`);
662
+ }
663
+
664
+ return [value, result, error];
665
+ }
666
+
667
+ // Write methods
668
+ async writeTxRx(port, scsId, address, length, data) {
669
+ if (scsId >= BROADCAST_ID) {
670
+ return [COMM_NOT_AVAILABLE, 0];
671
+ }
672
+
673
+ // Create write packet
674
+ const txpacket = new Array(length + 7).fill(0);
675
+ txpacket[PKT_ID] = scsId;
676
+ txpacket[PKT_LENGTH] = length + 3;
677
+ txpacket[PKT_INSTRUCTION] = INST_WRITE;
678
+ txpacket[PKT_PARAMETER0] = address;
679
+
680
+ // Add data
681
+ for (let i = 0; i < length; i++) {
682
+ txpacket[PKT_PARAMETER0 + 1 + i] = data[i] & 0xFF;
683
+ }
684
+
685
+ // Send packet and get response
686
+ const [rxpacket, result, error] = await this.txRxPacket(port, txpacket);
687
+
688
+ return [result, error];
689
+ }
690
+
691
+ async write1ByteTxRx(port, scsId, address, data) {
692
+ const dataArray = [data & 0xFF];
693
+ return await this.writeTxRx(port, scsId, address, 1, dataArray);
694
+ }
695
+
696
+ async write2ByteTxRx(port, scsId, address, data) {
697
+ const dataArray = [
698
+ SCS_LOBYTE(data),
699
+ SCS_HIBYTE(data)
700
+ ];
701
+ return await this.writeTxRx(port, scsId, address, 2, dataArray);
702
+ }
703
+
704
+ async write4ByteTxRx(port, scsId, address, data) {
705
+ const dataArray = [
706
+ SCS_LOBYTE(SCS_LOWORD(data)),
707
+ SCS_HIBYTE(SCS_LOWORD(data)),
708
+ SCS_LOBYTE(SCS_HIWORD(data)),
709
+ SCS_HIBYTE(SCS_HIWORD(data))
710
+ ];
711
+ return await this.writeTxRx(port, scsId, address, 4, dataArray);
712
+ }
713
+
714
+ // Add syncReadTx for GroupSyncRead functionality
715
+ async syncReadTx(port, startAddress, dataLength, param, paramLength) {
716
+ // Create packet: HEADER0 HEADER1 ID LEN INST START_ADDR DATA_LEN PARAM... CHKSUM
717
+ const txpacket = new Array(paramLength + 8).fill(0);
718
+
719
+ txpacket[PKT_ID] = BROADCAST_ID;
720
+ txpacket[PKT_LENGTH] = paramLength + 4; // 4: INST START_ADDR DATA_LEN CHKSUM
721
+ txpacket[PKT_INSTRUCTION] = INST_SYNC_READ;
722
+ txpacket[PKT_PARAMETER0] = startAddress;
723
+ txpacket[PKT_PARAMETER0 + 1] = dataLength;
724
+
725
+ // Add parameters
726
+ for (let i = 0; i < paramLength; i++) {
727
+ txpacket[PKT_PARAMETER0 + 2 + i] = param[i];
728
+ }
729
+
730
+ // Calculate checksum
731
+ const totalLen = txpacket[PKT_LENGTH] + 4; // 4: HEADER0 HEADER1 ID LENGTH
732
+
733
+ // Add headers
734
+ txpacket[PKT_HEADER0] = 0xFF;
735
+ txpacket[PKT_HEADER1] = 0xFF;
736
+
737
+ // Calculate checksum
738
+ let checksum = 0;
739
+ for (let i = 2; i < totalLen - 1; i++) {
740
+ checksum += txpacket[i] & 0xFF;
741
+ }
742
+ txpacket[totalLen - 1] = (~checksum) & 0xFF;
743
+
744
+ console.log(`SyncReadTx: ${txpacket.map(b => '0x' + b.toString(16).padStart(2,'0')).join(' ')}`);
745
+
746
+ // Send packet
747
+ await port.clearPort();
748
+ const bytesWritten = await port.writePort(txpacket);
749
+ if (bytesWritten !== totalLen) {
750
+ return COMM_TX_FAIL;
751
+ }
752
+
753
+ // Set timeout based on expected response size
754
+ port.setPacketTimeout((6 + dataLength) * paramLength);
755
+
756
+ return COMM_SUCCESS;
757
+ }
758
+
759
+ // Add syncWriteTxOnly for GroupSyncWrite functionality
760
+ async syncWriteTxOnly(port, startAddress, dataLength, param, paramLength) {
761
+ // Create packet: HEADER0 HEADER1 ID LEN INST START_ADDR DATA_LEN PARAM... CHKSUM
762
+ const txpacket = new Array(paramLength + 8).fill(0);
763
+
764
+ txpacket[PKT_ID] = BROADCAST_ID;
765
+ txpacket[PKT_LENGTH] = paramLength + 4; // 4: INST START_ADDR DATA_LEN CHKSUM
766
+ txpacket[PKT_INSTRUCTION] = INST_SYNC_WRITE;
767
+ txpacket[PKT_PARAMETER0] = startAddress;
768
+ txpacket[PKT_PARAMETER0 + 1] = dataLength;
769
+
770
+ // Add parameters
771
+ for (let i = 0; i < paramLength; i++) {
772
+ txpacket[PKT_PARAMETER0 + 2 + i] = param[i];
773
+ }
774
+
775
+ // Calculate checksum
776
+ const totalLen = txpacket[PKT_LENGTH] + 4; // 4: HEADER0 HEADER1 ID LENGTH
777
+
778
+ // Add headers
779
+ txpacket[PKT_HEADER0] = 0xFF;
780
+ txpacket[PKT_HEADER1] = 0xFF;
781
+
782
+ // Calculate checksum
783
+ let checksum = 0;
784
+ for (let i = 2; i < totalLen - 1; i++) {
785
+ checksum += txpacket[i] & 0xFF;
786
+ }
787
+ txpacket[totalLen - 1] = (~checksum) & 0xFF;
788
+
789
+ // console.log(`SyncWriteTxOnly: ${txpacket.map(b => '0x' + b.toString(16).padStart(2,'0')).join(' ')}`);
790
+
791
+ // Send packet - for sync write, we don't need a response
792
+ await port.clearPort();
793
+ const bytesWritten = await port.writePort(txpacket);
794
+ if (bytesWritten !== totalLen) {
795
+ return COMM_TX_FAIL;
796
+ }
797
+
798
+ return COMM_SUCCESS;
799
+ }
800
+
801
+ // 辅助方法:格式化数据包结构以方便调试
802
+ formatPacketStructure(packet) {
803
+ if (!packet || packet.length < 4) {
804
+ return "Invalid packet (too short)";
805
+ }
806
+
807
+ try {
808
+ let result = "";
809
+ result += `HEADER: ${packet[0].toString(16).padStart(2,'0')} ${packet[1].toString(16).padStart(2,'0')} | `;
810
+ result += `ID: ${packet[2]} | `;
811
+ result += `LENGTH: ${packet[3]} | `;
812
+
813
+ if (packet.length >= 5) {
814
+ result += `ERROR/INST: ${packet[4].toString(16).padStart(2,'0')} | `;
815
+ }
816
+
817
+ if (packet.length >= 6) {
818
+ result += "PARAMS: ";
819
+ for (let i = 5; i < packet.length - 1; i++) {
820
+ result += `${packet[i].toString(16).padStart(2,'0')} `;
821
+ }
822
+ result += `| CHECKSUM: ${packet[packet.length-1].toString(16).padStart(2,'0')}`;
823
+ }
824
+
825
+ return result;
826
+ } catch (e) {
827
+ return "Error formatting packet: " + e.message;
828
+ }
829
+ }
830
+
831
+ /**
832
+ * 从响应包中解析舵机型号
833
+ * @param {Array} rxpacket - 响应数据包
834
+ * @returns {number} 舵机型号
835
+ */
836
+ parseModelNumber(rxpacket) {
837
+ if (!rxpacket || rxpacket.length < 7) {
838
+ return 0;
839
+ }
840
+
841
+ // 检查是否有参数字段
842
+ if (rxpacket.length <= PKT_PARAMETER0 + 1) {
843
+ return 0;
844
+ }
845
+
846
+ const param1 = rxpacket[PKT_PARAMETER0];
847
+ const param2 = rxpacket[PKT_PARAMETER0 + 1];
848
+
849
+ if (SCS_END === 0) {
850
+ // STS/SMS 协议的字节顺序
851
+ return SCS_MAKEWORD(param1, param2);
852
+ } else {
853
+ // SCS 协议的字节顺序
854
+ return SCS_MAKEWORD(param2, param1);
855
+ }
856
+ }
857
+
858
+ /**
859
+ * Verify packet header
860
+ * @param {Array} packet - The packet to verify
861
+ * @returns {Number} COMM_SUCCESS if packet is valid, error code otherwise
862
+ */
863
+ getPacketHeader(packet) {
864
+ if (!packet || packet.length < 4) {
865
+ return COMM_RX_CORRUPT;
866
+ }
867
+
868
+ // Check header
869
+ if (packet[PKT_HEADER0] !== 0xFF || packet[PKT_HEADER1] !== 0xFF) {
870
+ return COMM_RX_CORRUPT;
871
+ }
872
+
873
+ // Check ID validity
874
+ if (packet[PKT_ID] > 0xFD) {
875
+ return COMM_RX_CORRUPT;
876
+ }
877
+
878
+ // Check length
879
+ if (packet.length != (packet[PKT_LENGTH] + 4)) {
880
+ return COMM_RX_CORRUPT;
881
+ }
882
+
883
+ // Calculate checksum
884
+ let checksum = 0;
885
+ for (let i = 2; i < packet.length - 1; i++) {
886
+ checksum += packet[i] & 0xFF;
887
+ }
888
+ checksum = (~checksum) & 0xFF;
889
+
890
+ // Verify checksum
891
+ if (packet[packet.length - 1] !== checksum) {
892
+ return COMM_RX_CORRUPT;
893
+ }
894
+
895
+ return COMM_SUCCESS;
896
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
897
  }
898
 
899
  /**
 
901
  * - This class is used to read multiple servos with the same control table address at once
902
  */
903
  export class GroupSyncRead {
904
+ constructor(port, ph, startAddress, dataLength) {
905
+ this.port = port;
906
+ this.ph = ph;
907
+ this.startAddress = startAddress;
908
+ this.dataLength = dataLength;
909
+
910
+ this.isAvailableServiceID = new Set();
911
+ this.dataDict = new Map();
912
+ this.param = [];
913
+ this.clearParam();
914
+ }
915
+
916
+ makeParam() {
917
+ this.param = [];
918
+ for (const id of this.isAvailableServiceID) {
919
+ this.param.push(id);
920
+ }
921
+ return this.param.length;
922
+ }
923
+
924
+ addParam(scsId) {
925
+ if (this.isAvailableServiceID.has(scsId)) {
926
+ return false;
927
+ }
928
+
929
+ this.isAvailableServiceID.add(scsId);
930
+ this.dataDict.set(scsId, new Array(this.dataLength).fill(0));
931
+ return true;
932
+ }
933
+
934
+ removeParam(scsId) {
935
+ if (!this.isAvailableServiceID.has(scsId)) {
936
+ return false;
937
+ }
938
+
939
+ this.isAvailableServiceID.delete(scsId);
940
+ this.dataDict.delete(scsId);
941
+ return true;
942
+ }
943
+
944
+ clearParam() {
945
+ this.isAvailableServiceID.clear();
946
+ this.dataDict.clear();
947
+ return true;
948
+ }
949
+
950
+ async txPacket() {
951
+ if (this.isAvailableServiceID.size === 0) {
952
+ return COMM_NOT_AVAILABLE;
953
+ }
954
+
955
+ const paramLength = this.makeParam();
956
+ return await this.ph.syncReadTx(this.port, this.startAddress, this.dataLength, this.param, paramLength);
957
+ }
958
+
959
+ async rxPacket() {
960
+ let result = COMM_RX_FAIL;
961
+
962
+ if (this.isAvailableServiceID.size === 0) {
963
+ return COMM_NOT_AVAILABLE;
964
+ }
965
+
966
+ // Set all servos' data as invalid
967
+ for (const id of this.isAvailableServiceID) {
968
+ this.dataDict.set(id, new Array(this.dataLength).fill(0));
969
+ console.log(`Cleared data for servo ID ${id}`);
970
+ }
971
+
972
+ const [rxpacket, rxResult] = await this.ph.rxPacket(this.port);
973
+ if (rxResult !== COMM_SUCCESS || !rxpacket || rxpacket.length === 0) {
974
+ return rxResult;
975
+ }
976
+
977
+ // More tolerant of packets with unexpected values in the PKT_ERROR field
978
+ // Don't require INST_STATUS to be exactly 0x55
979
+ console.log(`GroupSyncRead rxPacket: ID=${rxpacket[PKT_ID]}, ERROR/INST field=0x${rxpacket[PKT_ERROR].toString(16)}`);
980
+
981
+ // Check if the packet matches any of the available IDs
982
+ if (!this.isAvailableServiceID.has(rxpacket[PKT_ID])) {
983
+ console.log(`Received packet with ID ${rxpacket[PKT_ID]} which is not in the available IDs list`);
984
+ return COMM_RX_CORRUPT;
985
+ }
986
+
987
+ // Extract data for the matching ID
988
+ const scsId = rxpacket[PKT_ID];
989
+ const data = new Array(this.dataLength).fill(0);
990
+
991
+ // Extract the parameter data, which should start at PKT_PARAMETER0
992
+ if (rxpacket.length < PKT_PARAMETER0 + this.dataLength) {
993
+ console.log(`Packet too short: expected ${PKT_PARAMETER0 + this.dataLength} bytes, got ${rxpacket.length}`);
994
+ return COMM_RX_CORRUPT;
995
+ }
996
+
997
+ for (let i = 0; i < this.dataLength; i++) {
998
+ data[i] = rxpacket[PKT_PARAMETER0 + i];
999
+ }
1000
+
1001
+ // Update the data dict
1002
+ this.dataDict.set(scsId, data);
1003
+ console.log(`Updated data for servo ID ${scsId}: ${data.map(b => '0x' + b.toString(16).padStart(2,'0')).join(' ')}`);
1004
+
1005
+ // Continue receiving until timeout or all data is received
1006
+ if (this.isAvailableServiceID.size > 1) {
1007
+ result = await this.rxPacket();
1008
+ } else {
1009
+ result = COMM_SUCCESS;
1010
+ }
1011
+
1012
+ return result;
1013
+ }
1014
+
1015
+ async txRxPacket() {
1016
+ try {
1017
+ // First check if port is being used
1018
+ if (this.port.isUsing) {
1019
+ console.log("Port is busy, cannot start sync read operation");
1020
+ return COMM_PORT_BUSY;
1021
+ }
1022
+
1023
+ // Start the transmission
1024
+ console.log("Starting sync read TX/RX operation...");
1025
+ let result = await this.txPacket();
1026
+ if (result !== COMM_SUCCESS) {
1027
+ console.log(`Sync read TX failed with result: ${result}`);
1028
+ return result;
1029
+ }
1030
+
1031
+ // Get a single response with a standard timeout
1032
+ console.log(`Attempting to receive a response...`);
1033
+
1034
+ // Receive a single response
1035
+ result = await this.rxPacket();
1036
+ console.log(`Sync read RX result###: ${result}`);
1037
+ // Release port
1038
+ this.port.isUsing = false;
1039
+
1040
+ return result;
1041
+ } catch (error) {
1042
+ console.error("Exception in GroupSyncRead txRxPacket:", error);
1043
+ // Make sure port is released
1044
+ this.port.isUsing = false;
1045
+ return COMM_RX_FAIL;
1046
+ }
1047
+ }
1048
+
1049
+ isAvailable(scsId, address, dataLength) {
1050
+ if (!this.isAvailableServiceID.has(scsId)) {
1051
+ return false;
1052
+ }
1053
+
1054
+ const startAddr = this.startAddress;
1055
+ const endAddr = startAddr + this.dataLength - 1;
1056
+
1057
+ const reqStartAddr = address;
1058
+ const reqEndAddr = reqStartAddr + dataLength - 1;
1059
+
1060
+ if (reqStartAddr < startAddr || reqEndAddr > endAddr) {
1061
+ return false;
1062
+ }
1063
+
1064
+ const data = this.dataDict.get(scsId);
1065
+ if (!data || data.length === 0) {
1066
+ return false;
1067
+ }
1068
+
1069
+ return true;
1070
+ }
1071
+
1072
+ getData(scsId, address, dataLength) {
1073
+ if (!this.isAvailable(scsId, address, dataLength)) {
1074
+ return 0;
1075
+ }
1076
+
1077
+ const startAddr = this.startAddress;
1078
+ const data = this.dataDict.get(scsId);
1079
+
1080
+ // Calculate data offset
1081
+ const dataOffset = address - startAddr;
1082
+
1083
+ // Combine bytes according to dataLength
1084
+ switch (dataLength) {
1085
+ case 1:
1086
+ return data[dataOffset];
1087
+ case 2:
1088
+ return SCS_MAKEWORD(data[dataOffset], data[dataOffset + 1]);
1089
+ case 4:
1090
+ return SCS_MAKEDWORD(
1091
+ SCS_MAKEWORD(data[dataOffset], data[dataOffset + 1]),
1092
+ SCS_MAKEWORD(data[dataOffset + 2], data[dataOffset + 3])
1093
+ );
1094
+ default:
1095
+ return 0;
1096
+ }
1097
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
1098
  }
1099
 
1100
  /**
 
1102
  * - This class is used to write multiple servos with the same control table address at once
1103
  */
1104
  export class GroupSyncWrite {
1105
+ constructor(port, ph, startAddress, dataLength) {
1106
+ this.port = port;
1107
+ this.ph = ph;
1108
+ this.startAddress = startAddress;
1109
+ this.dataLength = dataLength;
1110
+
1111
+ this.isAvailableServiceID = new Set();
1112
+ this.dataDict = new Map();
1113
+ this.param = [];
1114
+ this.clearParam();
1115
+ }
1116
+
1117
+ makeParam() {
1118
+ this.param = [];
1119
+ for (const id of this.isAvailableServiceID) {
1120
+ // Add ID to parameter
1121
+ this.param.push(id);
1122
+
1123
+ // Add data to parameter
1124
+ const data = this.dataDict.get(id);
1125
+ for (let i = 0; i < this.dataLength; i++) {
1126
+ this.param.push(data[i]);
1127
+ }
1128
+ }
1129
+ return this.param.length;
1130
+ }
1131
+
1132
+ addParam(scsId, data) {
1133
+ if (this.isAvailableServiceID.has(scsId)) {
1134
+ return false;
1135
+ }
1136
+
1137
+ if (data.length !== this.dataLength) {
1138
+ console.error(`Data length (${data.length}) doesn't match required length (${this.dataLength})`);
1139
+ return false;
1140
+ }
1141
+
1142
+ this.isAvailableServiceID.add(scsId);
1143
+ this.dataDict.set(scsId, data);
1144
+ return true;
1145
+ }
1146
+
1147
+ removeParam(scsId) {
1148
+ if (!this.isAvailableServiceID.has(scsId)) {
1149
+ return false;
1150
+ }
1151
+
1152
+ this.isAvailableServiceID.delete(scsId);
1153
+ this.dataDict.delete(scsId);
1154
+ return true;
1155
+ }
1156
+
1157
+ changeParam(scsId, data) {
1158
+ if (!this.isAvailableServiceID.has(scsId)) {
1159
+ return false;
1160
+ }
1161
+
1162
+ if (data.length !== this.dataLength) {
1163
+ console.error(`Data length (${data.length}) doesn't match required length (${this.dataLength})`);
1164
+ return false;
1165
+ }
1166
+
1167
+ this.dataDict.set(scsId, data);
1168
+ return true;
1169
+ }
1170
+
1171
+ clearParam() {
1172
+ this.isAvailableServiceID.clear();
1173
+ this.dataDict.clear();
1174
+ return true;
1175
+ }
1176
+
1177
+ async txPacket() {
1178
+ if (this.isAvailableServiceID.size === 0) {
1179
+ return COMM_NOT_AVAILABLE;
1180
+ }
1181
+
1182
+ const paramLength = this.makeParam();
1183
+ return await this.ph.syncWriteTxOnly(this.port, this.startAddress, this.dataLength, this.param, paramLength);
1184
+ }
 
 
 
 
 
 
 
 
 
 
1185
  }
packages/feetech.js/package.json CHANGED
@@ -1,38 +1,38 @@
1
  {
2
- "name": "feetech.js",
3
- "version": "0.0.11",
4
- "description": "javascript sdk for feetech servos",
5
- "main": "index.mjs",
6
- "files": [
7
- "*.mjs",
8
- "*.ts"
9
- ],
10
- "type": "module",
11
- "engines": {
12
- "node": ">=12.17.0"
13
- },
14
- "scripts": {
15
- "test": "echo \"Error: no test specified\" && exit 1"
16
- },
17
- "repository": {
18
- "type": "git",
19
- "url": "git+https://github.com/julien-blanchon/robothub.git#main:frontend/packages/feetech.js"
20
- },
21
- "keywords": [
22
- "feetech",
23
- "sdk",
24
- "js",
25
- "javascript",
26
- "sts3215",
27
- "3215",
28
- "scs",
29
- "scs3215",
30
- "st3215"
31
- ],
32
- "author": "timqian",
33
- "license": "MIT",
34
- "bugs": {
35
- "url": "https://github.com/julien-blanchon/robothub.git#main:frontend/packages/feetech.js"
36
- },
37
- "homepage": "https://github.com/julien-blanchon/robothub.git#main:frontend/packages/feetech.js"
38
  }
 
1
  {
2
+ "name": "feetech.js",
3
+ "version": "0.1.2",
4
+ "description": "javascript sdk for feetech servos",
5
+ "main": "index.mjs",
6
+ "files": [
7
+ "*.mjs",
8
+ "*.ts"
9
+ ],
10
+ "type": "module",
11
+ "engines": {
12
+ "node": ">=12.17.0"
13
+ },
14
+ "scripts": {
15
+ "test": "echo \"Error: no test specified\" && exit 1"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/timqian/bambot/tree/main/feetech.js"
20
+ },
21
+ "keywords": [
22
+ "feetech",
23
+ "sdk",
24
+ "js",
25
+ "javascript",
26
+ "sts3215",
27
+ "3215",
28
+ "scs",
29
+ "scs3215",
30
+ "st3215"
31
+ ],
32
+ "author": "timqian",
33
+ "license": "MIT",
34
+ "bugs": {
35
+ "url": "https://github.com/timqian/bambot/issues"
36
+ },
37
+ "homepage": "https://github.com/timqian/bambot/tree/main/feetech.js"
38
  }
packages/feetech.js/scsServoSDK.mjs CHANGED
@@ -1,1205 +1,734 @@
1
  import {
2
- PortHandler,
3
- PacketHandler,
4
- COMM_SUCCESS,
5
- COMM_RX_TIMEOUT,
6
- COMM_RX_CORRUPT,
7
- COMM_TX_FAIL,
8
- COMM_NOT_AVAILABLE,
9
- SCS_LOBYTE,
10
- SCS_HIBYTE,
11
- SCS_MAKEWORD,
12
- GroupSyncRead, // Import GroupSyncRead
13
- GroupSyncWrite // Import GroupSyncWrite
 
14
  } from "./lowLevelSDK.mjs";
15
 
16
  // Import address constants from the correct file
17
  import {
18
- ADDR_SCS_PRESENT_POSITION,
19
- ADDR_SCS_GOAL_POSITION,
20
- ADDR_SCS_TORQUE_ENABLE,
21
- ADDR_SCS_GOAL_ACC,
22
- ADDR_SCS_GOAL_SPEED
23
  } from "./scsservo_constants.mjs";
24
 
25
- // Import debug logging function
26
- import { debugLog } from "./debug.mjs";
27
-
28
  // Define constants not present in scsservo_constants.mjs
29
  const ADDR_SCS_MODE = 33;
30
  const ADDR_SCS_LOCK = 55;
31
  const ADDR_SCS_ID = 5; // Address for Servo ID
32
  const ADDR_SCS_BAUD_RATE = 6; // Address for Baud Rate
33
 
34
- // Module-level variables for handlers
35
- let portHandler = null;
36
- let packetHandler = null;
37
-
38
- /**
39
- * Unified Servo SDK with flexible locking control
40
- * Supports both locked (respects servo locks) and unlocked (temporary unlock) operations
41
- */
42
-
43
- /**
44
- * Connects to the serial port and initializes handlers.
45
- * @param {object} [options] - Connection options.
46
- * @param {number} [options.baudRate=1000000] - The baud rate for the serial connection.
47
- * @param {number} [options.protocolEnd=0] - The protocol end setting (0 for STS/SMS, 1 for SCS).
48
- * @returns {Promise<true>} Resolves with true on successful connection.
49
- * @throws {Error} If connection fails or port cannot be opened/selected.
50
- */
51
- export async function connect(options = {}) {
52
- if (portHandler && portHandler.isOpen) {
53
- debugLog("Already connected to servo system.");
54
- return true;
55
- }
56
-
57
- const { baudRate = 1000000, protocolEnd = 0 } = options;
58
-
59
- try {
60
- portHandler = new PortHandler();
61
- const portRequested = await portHandler.requestPort();
62
- if (!portRequested) {
63
- portHandler = null;
64
- throw new Error("Failed to select a serial port.");
65
- }
66
-
67
- portHandler.setBaudRate(baudRate);
68
- const portOpened = await portHandler.openPort();
69
- if (!portOpened) {
70
- await portHandler.closePort().catch(console.error);
71
- portHandler = null;
72
- throw new Error(`Failed to open port at baudrate ${baudRate}.`);
73
- }
74
-
75
- packetHandler = new PacketHandler(protocolEnd);
76
- debugLog(`Connected to servo system at ${baudRate} baud, protocol end: ${protocolEnd}.`);
77
- return true;
78
- } catch (err) {
79
- console.error("Error during servo connection:", err);
80
- if (portHandler) {
81
- try {
82
- await portHandler.closePort();
83
- } catch (closeErr) {
84
- console.error("Error closing port after connection failure:", closeErr);
85
- }
86
- }
87
- portHandler = null;
88
- packetHandler = null;
89
- throw new Error(`Servo connection failed: ${err.message}`);
90
- }
91
- }
92
-
93
- /**
94
- * Disconnects from the serial port.
95
- * @returns {Promise<true>} Resolves with true on successful disconnection.
96
- * @throws {Error} If disconnection fails.
97
- */
98
- export async function disconnect() {
99
- if (!portHandler || !portHandler.isOpen) {
100
- debugLog("Already disconnected from servo system.");
101
- return true;
102
- }
103
-
104
- try {
105
- await portHandler.closePort();
106
- portHandler = null;
107
- packetHandler = null;
108
- debugLog("Disconnected from servo system.");
109
- return true;
110
- } catch (err) {
111
- console.error("Error during servo disconnection:", err);
112
- portHandler = null;
113
- packetHandler = null;
114
- throw new Error(`Servo disconnection failed: ${err.message}`);
115
- }
116
- }
117
-
118
- /**
119
- * Checks if the SDK is currently connected.
120
- * @returns {boolean} True if connected, false otherwise.
121
- */
122
- export function isConnected() {
123
- return !!(portHandler && portHandler.isOpen && packetHandler);
124
- }
125
-
126
- /**
127
- * Checks if the SDK is connected. Throws an error if not.
128
- * @throws {Error} If not connected.
129
- */
130
- function checkConnection() {
131
- if (!portHandler || !packetHandler) {
132
- throw new Error("Not connected to servo system. Call connect() first.");
133
- }
134
- }
135
-
136
- // =============================================================================
137
- // SERVO LOCKING OPERATIONS
138
- // =============================================================================
139
-
140
- /**
141
- * Locks a servo to prevent configuration changes.
142
- * @param {number} servoId - The ID of the servo (1-252).
143
- * @returns {Promise<"success">} Resolves with "success".
144
- * @throws {Error} If not connected, write fails, or an exception occurs.
145
- */
146
- export async function lockServo(servoId) {
147
- checkConnection();
148
- try {
149
- debugLog(`🔒 Locking servo ${servoId}...`);
150
- const [result, error] = await packetHandler.write1ByteTxRx(
151
- portHandler,
152
- servoId,
153
- ADDR_SCS_LOCK,
154
- 1
155
- );
156
-
157
- if (result !== COMM_SUCCESS) {
158
- throw new Error(
159
- `Error locking servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error: ${error}`
160
- );
161
- }
162
- debugLog(`🔒 Servo ${servoId} locked successfully`);
163
- return "success";
164
- } catch (err) {
165
- console.error(`Exception locking servo ${servoId}:`, err);
166
- throw new Error(`Failed to lock servo ${servoId}: ${err.message}`);
167
- }
168
- }
169
-
170
- /**
171
- * Unlocks a servo to allow configuration changes.
172
- * @param {number} servoId - The ID of the servo (1-252).
173
- * @returns {Promise<"success">} Resolves with "success".
174
- * @throws {Error} If not connected, write fails, or an exception occurs.
175
- */
176
- export async function unlockServo(servoId) {
177
- checkConnection();
178
- try {
179
- debugLog(`🔓 Unlocking servo ${servoId}...`);
180
- const [result, error] = await packetHandler.write1ByteTxRx(
181
- portHandler,
182
- servoId,
183
- ADDR_SCS_LOCK,
184
- 0
185
- );
186
-
187
- if (result !== COMM_SUCCESS) {
188
- throw new Error(
189
- `Error unlocking servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error: ${error}`
190
- );
191
- }
192
- debugLog(`🔓 Servo ${servoId} unlocked successfully`);
193
- return "success";
194
- } catch (err) {
195
- console.error(`Exception unlocking servo ${servoId}:`, err);
196
- throw new Error(`Failed to unlock servo ${servoId}: ${err.message}`);
197
- }
198
- }
199
-
200
- /**
201
- * Locks multiple servos sequentially.
202
- * @param {number[]} servoIds - Array of servo IDs to lock.
203
- * @returns {Promise<"success">} Resolves with "success".
204
- * @throws {Error} If any servo fails to lock.
205
- */
206
- export async function lockServos(servoIds) {
207
- checkConnection();
208
- debugLog(`🔒 Locking ${servoIds.length} servos: [${servoIds.join(', ')}]`);
209
-
210
- // Lock servos sequentially to avoid port conflicts
211
- for (const servoId of servoIds) {
212
- await lockServo(servoId);
213
- }
214
-
215
- debugLog(`🔒 All ${servoIds.length} servos locked successfully`);
216
- return "success";
217
- }
218
-
219
- /**
220
- * Locks servos for production use by both locking configuration and enabling torque.
221
- * This ensures servos are truly locked and controlled by the system.
222
- * @param {number[]} servoIds - Array of servo IDs to lock for production.
223
- * @returns {Promise<"success">} Resolves with "success".
224
- * @throws {Error} If any servo fails to lock or enable torque.
225
- */
226
- export async function lockServosForProduction(servoIds) {
227
- checkConnection();
228
- debugLog(`🔒 Locking ${servoIds.length} servos for production use: [${servoIds.join(', ')}]`);
229
-
230
- // Lock servos sequentially and enable torque for each
231
- for (const servoId of servoIds) {
232
- try {
233
- debugLog(`🔒 Locking servo ${servoId} for production...`);
234
-
235
- // 1. Lock the servo configuration
236
- const [lockResult, lockError] = await packetHandler.write1ByteTxRx(
237
- portHandler,
238
- servoId,
239
- ADDR_SCS_LOCK,
240
- 1
241
- );
242
-
243
- if (lockResult !== COMM_SUCCESS) {
244
- throw new Error(`Error locking servo ${servoId}: ${packetHandler.getTxRxResult(lockResult)}, Error: ${lockError}`);
245
- }
246
-
247
- // 2. Enable torque to make servo controllable
248
- const [torqueResult, torqueError] = await packetHandler.write1ByteTxRx(
249
- portHandler,
250
- servoId,
251
- ADDR_SCS_TORQUE_ENABLE,
252
- 1
253
- );
254
-
255
- if (torqueResult !== COMM_SUCCESS) {
256
- console.warn(`Warning: Failed to enable torque for servo ${servoId}: ${packetHandler.getTxRxResult(torqueResult)}, Error: ${torqueError}`);
257
- // Don't throw here, locking is more important than torque enable
258
- }
259
-
260
- debugLog(`🔒 Servo ${servoId} locked and torque enabled for production`);
261
- } catch (err) {
262
- console.error(`Exception locking servo ${servoId} for production:`, err);
263
- throw new Error(`Failed to lock servo ${servoId} for production: ${err.message}`);
264
- }
265
- }
266
-
267
- debugLog(`🔒 All ${servoIds.length} servos locked for production successfully`);
268
- return "success";
269
- }
270
-
271
- /**
272
- * Unlocks multiple servos sequentially.
273
- * @param {number[]} servoIds - Array of servo IDs to unlock.
274
- * @returns {Promise<"success">} Resolves with "success".
275
- * @throws {Error} If any servo fails to unlock.
276
- */
277
- export async function unlockServos(servoIds) {
278
- checkConnection();
279
- debugLog(`🔓 Unlocking ${servoIds.length} servos: [${servoIds.join(', ')}]`);
280
-
281
- // Unlock servos sequentially to avoid port conflicts
282
- for (const servoId of servoIds) {
283
- await unlockServo(servoId);
284
- }
285
-
286
- debugLog(`🔓 All ${servoIds.length} servos unlocked successfully`);
287
- return "success";
288
- }
289
-
290
- /**
291
- * Safely unlocks servos for manual movement by unlocking configuration and disabling torque.
292
- * This is the safest way to leave servos when disconnecting/cleaning up.
293
- * @param {number[]} servoIds - Array of servo IDs to unlock safely.
294
- * @returns {Promise<"success">} Resolves with "success".
295
- * @throws {Error} If any servo fails to unlock or disable torque.
296
- */
297
- export async function unlockServosForManualMovement(servoIds) {
298
- checkConnection();
299
- debugLog(`🔓 Safely unlocking ${servoIds.length} servos for manual movement: [${servoIds.join(', ')}]`);
300
-
301
- // Unlock servos sequentially and disable torque for each
302
- for (const servoId of servoIds) {
303
- try {
304
- debugLog(`🔓 Safely unlocking servo ${servoId} for manual movement...`);
305
-
306
- // 1. Disable torque first (makes servo freely movable)
307
- const [torqueResult, torqueError] = await packetHandler.write1ByteTxRx(
308
- portHandler,
309
- servoId,
310
- ADDR_SCS_TORQUE_ENABLE,
311
- 0
312
- );
313
-
314
- if (torqueResult !== COMM_SUCCESS) {
315
- console.warn(`Warning: Failed to disable torque for servo ${servoId}: ${packetHandler.getTxRxResult(torqueResult)}, Error: ${torqueError}`);
316
- // Continue anyway, unlocking is more important
317
- }
318
-
319
- // 2. Unlock the servo configuration
320
- const [unlockResult, unlockError] = await packetHandler.write1ByteTxRx(
321
- portHandler,
322
- servoId,
323
- ADDR_SCS_LOCK,
324
- 0
325
- );
326
-
327
- if (unlockResult !== COMM_SUCCESS) {
328
- throw new Error(`Error unlocking servo ${servoId}: ${packetHandler.getTxRxResult(unlockResult)}, Error: ${unlockError}`);
329
- }
330
-
331
- debugLog(`🔓 Servo ${servoId} safely unlocked - torque disabled and configuration unlocked`);
332
- } catch (err) {
333
- console.error(`Exception safely unlocking servo ${servoId}:`, err);
334
- throw new Error(`Failed to safely unlock servo ${servoId}: ${err.message}`);
335
- }
336
- }
337
-
338
- debugLog(`🔓 All ${servoIds.length} servos safely unlocked for manual movement`);
339
- return "success";
340
- }
341
-
342
- // =============================================================================
343
- // READ OPERATIONS (No locking needed)
344
- // =============================================================================
345
-
346
- /**
347
- * Reads the current position of a servo.
348
- * @param {number} servoId - The ID of the servo (1-252).
349
- * @returns {Promise<number>} Resolves with the position (0-4095).
350
- * @throws {Error} If not connected, read fails, or an exception occurs.
351
- */
352
- export async function readPosition(servoId) {
353
- checkConnection();
354
- try {
355
- const [position, result, error] = await packetHandler.read2ByteTxRx(
356
- portHandler,
357
- servoId,
358
- ADDR_SCS_PRESENT_POSITION
359
- );
360
-
361
- if (result !== COMM_SUCCESS) {
362
- throw new Error(
363
- `Error reading position from servo ${servoId}: ${packetHandler.getTxRxResult(
364
- result
365
- )}, Error code: ${error}`
366
- );
367
- }
368
- return position & 0xffff;
369
- } catch (err) {
370
- console.error(`Exception reading position from servo ${servoId}:`, err);
371
- throw new Error(`Exception reading position from servo ${servoId}: ${err.message}`);
372
- }
373
- }
374
-
375
- /**
376
- * Reads the current baud rate index of a servo.
377
- * @param {number} servoId - The ID of the servo (1-252).
378
- * @returns {Promise<number>} Resolves with the baud rate index (0-7).
379
- * @throws {Error} If not connected, read fails, or an exception occurs.
380
- */
381
- export async function readBaudRate(servoId) {
382
- checkConnection();
383
- try {
384
- const [baudIndex, result, error] = await packetHandler.read1ByteTxRx(
385
- portHandler,
386
- servoId,
387
- ADDR_SCS_BAUD_RATE
388
- );
389
-
390
- if (result !== COMM_SUCCESS) {
391
- throw new Error(
392
- `Error reading baud rate from servo ${servoId}: ${packetHandler.getTxRxResult(
393
- result
394
- )}, Error code: ${error}`
395
- );
396
- }
397
- return baudIndex;
398
- } catch (err) {
399
- console.error(`Exception reading baud rate from servo ${servoId}:`, err);
400
- throw new Error(`Exception reading baud rate from servo ${servoId}: ${err.message}`);
401
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  }
403
 
404
- /**
405
- * Reads the current operating mode of a servo.
406
- * @param {number} servoId - The ID of the servo (1-252).
407
- * @returns {Promise<number>} Resolves with the mode (0 for position, 1 for wheel).
408
- * @throws {Error} If not connected, read fails, or an exception occurs.
409
- */
410
- export async function readMode(servoId) {
411
- checkConnection();
412
- try {
413
- const [modeValue, result, error] = await packetHandler.read1ByteTxRx(
414
- portHandler,
415
- servoId,
416
- ADDR_SCS_MODE
417
- );
418
-
419
- if (result !== COMM_SUCCESS) {
420
- throw new Error(
421
- `Error reading mode from servo ${servoId}: ${packetHandler.getTxRxResult(
422
- result
423
- )}, Error code: ${error}`
424
- );
425
- }
426
- return modeValue;
427
- } catch (err) {
428
- console.error(`Exception reading mode from servo ${servoId}:`, err);
429
- throw new Error(`Exception reading mode from servo ${servoId}: ${err.message}`);
430
- }
431
- }
432
-
433
- /**
434
- * Reads the current position of multiple servos synchronously.
435
- * @param {number[]} servoIds - An array of servo IDs (1-252) to read from.
436
- * @returns {Promise<Map<number, number>>} Resolves with a Map where keys are servo IDs and values are positions (0-4095).
437
- * @throws {Error} If not connected or transmission fails completely.
438
- */
439
- export async function syncReadPositions(servoIds) {
440
- checkConnection();
441
- if (!Array.isArray(servoIds) || servoIds.length === 0) {
442
- debugLog("Sync Read: No servo IDs provided.");
443
- return new Map();
444
- }
445
-
446
- const startAddress = ADDR_SCS_PRESENT_POSITION;
447
- const dataLength = 2;
448
- const groupSyncRead = new GroupSyncRead(portHandler, packetHandler, startAddress, dataLength);
449
- const positions = new Map();
450
- const validIds = [];
451
-
452
- // Add parameters for each valid servo ID
453
- servoIds.forEach((id) => {
454
- if (id >= 1 && id <= 252) {
455
- if (groupSyncRead.addParam(id)) {
456
- validIds.push(id);
457
- } else {
458
- console.warn(`Sync Read: Failed to add param for servo ID ${id} (maybe duplicate or invalid).`);
459
- }
460
- } else {
461
- console.warn(`Sync Read: Invalid servo ID ${id} skipped.`);
462
- }
463
- });
464
-
465
- if (validIds.length === 0) {
466
- debugLog("Sync Read: No valid servo IDs to read.");
467
- return new Map();
468
- }
469
-
470
- try {
471
- let txResult = await groupSyncRead.txPacket();
472
- if (txResult !== COMM_SUCCESS) {
473
- throw new Error(`Sync Read txPacket failed: ${packetHandler.getTxRxResult(txResult)}`);
474
- }
475
-
476
- let rxResult = await groupSyncRead.rxPacket();
477
- if (rxResult !== COMM_SUCCESS) {
478
- console.warn(`Sync Read rxPacket overall result: ${packetHandler.getTxRxResult(rxResult)}. Checking individual servos.`);
479
- }
480
-
481
- const failedIds = [];
482
- validIds.forEach((id) => {
483
- const isAvailable = groupSyncRead.isAvailable(id, startAddress, dataLength);
484
- if (isAvailable) {
485
- const position = groupSyncRead.getData(id, startAddress, dataLength);
486
- positions.set(id, position & 0xffff);
487
- } else {
488
- failedIds.push(id);
489
- }
490
- });
491
-
492
- if (failedIds.length > 0) {
493
- console.warn(`Sync Read: Data not available for servo IDs: ${failedIds.join(", ")}. Got ${positions.size}/${validIds.length} servos successfully.`);
494
- }
495
-
496
- return positions;
497
- } catch (err) {
498
- console.error("Exception during syncReadPositions:", err);
499
- throw new Error(`Sync Read failed: ${err.message}`);
500
- }
501
- }
502
-
503
- // =============================================================================
504
- // WRITE OPERATIONS - LOCKED MODE (Respects servo locks)
505
- // =============================================================================
506
-
507
- /**
508
- * Writes a target position to a servo (respects locks).
509
- * Will fail if the servo is locked.
510
- * @param {number} servoId - The ID of the servo (1-252).
511
- * @param {number} position - The target position value (0-4095).
512
- * @returns {Promise<"success">} Resolves with "success".
513
- * @throws {Error} If not connected, position is out of range, write fails, or an exception occurs.
514
- */
515
- export async function writePosition(servoId, position) {
516
- checkConnection();
517
- try {
518
- if (position < 0 || position > 4095) {
519
- throw new Error(`Invalid position value ${position} for servo ${servoId}. Must be between 0 and 4095.`);
520
- }
521
- const targetPosition = Math.round(position);
522
-
523
- const [result, error] = await packetHandler.write2ByteTxRx(
524
- portHandler,
525
- servoId,
526
- ADDR_SCS_GOAL_POSITION,
527
- targetPosition
528
- );
529
-
530
- if (result !== COMM_SUCCESS) {
531
- throw new Error(`Error writing position to servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error code: ${error}`);
532
- }
533
- return "success";
534
- } catch (err) {
535
- console.error(`Exception writing position to servo ${servoId}:`, err);
536
- throw new Error(`Failed to write position to servo ${servoId}: ${err.message}`);
537
- }
538
- }
539
-
540
- /**
541
- * Enables or disables the torque of a servo (respects locks).
542
- * @param {number} servoId - The ID of the servo (1-252).
543
- * @param {boolean} enable - True to enable torque, false to disable.
544
- * @returns {Promise<"success">} Resolves with "success".
545
- * @throws {Error} If not connected, write fails, or an exception occurs.
546
- */
547
- export async function writeTorqueEnable(servoId, enable) {
548
- checkConnection();
549
- try {
550
- const enableValue = enable ? 1 : 0;
551
- const [result, error] = await packetHandler.write1ByteTxRx(
552
- portHandler,
553
- servoId,
554
- ADDR_SCS_TORQUE_ENABLE,
555
- enableValue
556
- );
557
-
558
- if (result !== COMM_SUCCESS) {
559
- throw new Error(`Error setting torque for servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error code: ${error}`);
560
- }
561
- return "success";
562
- } catch (err) {
563
- console.error(`Exception setting torque for servo ${servoId}:`, err);
564
- throw new Error(`Exception setting torque for servo ${servoId}: ${err.message}`);
565
- }
566
- }
567
-
568
- // =============================================================================
569
- // WRITE OPERATIONS - UNLOCKED MODE (Temporary unlock for operation)
570
- // =============================================================================
571
-
572
- /**
573
- * Helper to attempt locking a servo, logging errors without throwing.
574
- * @param {number} servoId
575
- */
576
- async function tryLockServo(servoId) {
577
- try {
578
- await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1);
579
- } catch (lockErr) {
580
- console.error(`Failed to re-lock servo ${servoId}:`, lockErr);
581
- }
582
- }
583
-
584
- /**
585
- * Writes a target position to a servo with temporary unlocking.
586
- * Temporarily unlocks the servo, writes the position, then locks it back.
587
- * @param {number} servoId - The ID of the servo (1-252).
588
- * @param {number} position - The target position value (0-4095).
589
- * @returns {Promise<"success">} Resolves with "success".
590
- * @throws {Error} If not connected, position is out of range, write fails, or an exception occurs.
591
- */
592
- export async function writePositionUnlocked(servoId, position) {
593
- checkConnection();
594
- let unlocked = false;
595
- try {
596
- if (position < 0 || position > 4095) {
597
- throw new Error(`Invalid position value ${position} for servo ${servoId}. Must be between 0 and 4095.`);
598
- }
599
- const targetPosition = Math.round(position);
600
-
601
- debugLog(`🔓 Temporarily unlocking servo ${servoId} for position write...`);
602
-
603
- // 1. Unlock servo configuration first
604
- const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0);
605
- if (resUnlock !== COMM_SUCCESS) {
606
- debugLog(`Warning: Failed to unlock servo ${servoId}, trying direct write anyway...`);
607
- } else {
608
- unlocked = true;
609
- }
610
-
611
- // 2. Write the position
612
- const [result, error] = await packetHandler.write2ByteTxRx(portHandler, servoId, ADDR_SCS_GOAL_POSITION, targetPosition);
613
- if (result !== COMM_SUCCESS) {
614
- throw new Error(`Error writing position to servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error code: ${error}`);
615
- }
616
-
617
- // 3. Lock servo configuration back
618
- if (unlocked) {
619
- const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1);
620
- if (resLock !== COMM_SUCCESS) {
621
- console.warn(`Warning: Failed to re-lock servo ${servoId} after position write: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`);
622
- } else {
623
- unlocked = false;
624
- }
625
- }
626
-
627
- return "success";
628
- } catch (err) {
629
- console.error(`Exception writing position to servo ${servoId}:`, err);
630
- if (unlocked) {
631
- await tryLockServo(servoId);
632
- }
633
- throw new Error(`Failed to write position to servo ${servoId}: ${err.message}`);
634
- }
635
- }
636
-
637
- /**
638
- * Writes a target position and disables torque for manual movement.
639
- * @param {number} servoId - The ID of the servo (1-252).
640
- * @param {number} position - The target position value (0-4095).
641
- * @param {number} waitTimeMs - Time to wait for servo to reach position (milliseconds).
642
- * @returns {Promise<"success">} Resolves with "success".
643
- * @throws {Error} If not connected, position is out of range, write fails, or an exception occurs.
644
- */
645
- export async function writePositionAndDisableTorque(servoId, position, waitTimeMs = 1500) {
646
- checkConnection();
647
- let unlocked = false;
648
- try {
649
- if (position < 0 || position > 4095) {
650
- throw new Error(`Invalid position value ${position} for servo ${servoId}. Must be between 0 and 4095.`);
651
- }
652
- const targetPosition = Math.round(position);
653
-
654
- debugLog(`🔓 Moving servo ${servoId} to position ${targetPosition}, waiting ${waitTimeMs}ms, then disabling torque...`);
655
-
656
- // 1. Unlock servo configuration first
657
- const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0);
658
- if (resUnlock !== COMM_SUCCESS) {
659
- debugLog(`Warning: Failed to unlock servo ${servoId}, trying direct write anyway...`);
660
- } else {
661
- unlocked = true;
662
- }
663
-
664
- // 2. Enable torque first
665
- const [torqueEnableResult, torqueEnableError] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_TORQUE_ENABLE, 1);
666
- if (torqueEnableResult !== COMM_SUCCESS) {
667
- console.warn(`Warning: Failed to enable torque for servo ${servoId}: ${packetHandler.getTxRxResult(torqueEnableResult)}, Error: ${torqueEnableError}`);
668
- } else {
669
- debugLog(`✅ Torque enabled for servo ${servoId}`);
670
- }
671
-
672
- // 3. Write the position
673
- const [result, error] = await packetHandler.write2ByteTxRx(portHandler, servoId, ADDR_SCS_GOAL_POSITION, targetPosition);
674
- if (result !== COMM_SUCCESS) {
675
- throw new Error(`Error writing position to servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error code: ${error}`);
676
- }
677
-
678
- // 4. Wait for servo to reach position
679
- debugLog(`⏳ Waiting ${waitTimeMs}ms for servo ${servoId} to reach position ${targetPosition}...`);
680
- await new Promise(resolve => setTimeout(resolve, waitTimeMs));
681
-
682
- // 5. Disable torque
683
- const [torqueResult, torqueError] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_TORQUE_ENABLE, 0);
684
- if (torqueResult !== COMM_SUCCESS) {
685
- console.warn(`Warning: Failed to disable torque for servo ${servoId}: ${packetHandler.getTxRxResult(torqueResult)}, Error: ${torqueError}`);
686
- } else {
687
- debugLog(`✅ Torque disabled for servo ${servoId} - now movable by hand`);
688
- }
689
-
690
- // 6. Lock servo configuration back
691
- if (unlocked) {
692
- const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1);
693
- if (resLock !== COMM_SUCCESS) {
694
- console.warn(`Warning: Failed to re-lock servo ${servoId} after position write: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`);
695
- } else {
696
- unlocked = false;
697
- }
698
- }
699
-
700
- return "success";
701
- } catch (err) {
702
- console.error(`Exception writing position and disabling torque for servo ${servoId}:`, err);
703
- if (unlocked) {
704
- await tryLockServo(servoId);
705
- }
706
- throw new Error(`Failed to write position and disable torque for servo ${servoId}: ${err.message}`);
707
- }
708
- }
709
-
710
- /**
711
- * Enables or disables the torque of a servo with temporary unlocking.
712
- * @param {number} servoId - The ID of the servo (1-252).
713
- * @param {boolean} enable - True to enable torque, false to disable.
714
- * @returns {Promise<"success">} Resolves with "success".
715
- * @throws {Error} If not connected, write fails, or an exception occurs.
716
- */
717
- export async function writeTorqueEnableUnlocked(servoId, enable) {
718
- checkConnection();
719
- let unlocked = false;
720
- try {
721
- const enableValue = enable ? 1 : 0;
722
-
723
- debugLog(`🔓 Temporarily unlocking servo ${servoId} for torque enable write...`);
724
-
725
- // 1. Unlock servo configuration first
726
- const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0);
727
- if (resUnlock !== COMM_SUCCESS) {
728
- debugLog(`Warning: Failed to unlock servo ${servoId}, trying direct write anyway...`);
729
- } else {
730
- unlocked = true;
731
- }
732
-
733
- // 2. Write the torque enable
734
- const [result, error] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_TORQUE_ENABLE, enableValue);
735
- if (result !== COMM_SUCCESS) {
736
- throw new Error(`Error setting torque for servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error code: ${error}`);
737
- }
738
-
739
- // 3. Lock servo configuration back
740
- if (unlocked) {
741
- const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1);
742
- if (resLock !== COMM_SUCCESS) {
743
- console.warn(`Warning: Failed to re-lock servo ${servoId} after torque enable write: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`);
744
- } else {
745
- unlocked = false;
746
- }
747
- }
748
-
749
- return "success";
750
- } catch (err) {
751
- console.error(`Exception setting torque for servo ${servoId}:`, err);
752
- if (unlocked) {
753
- await tryLockServo(servoId);
754
- }
755
- throw new Error(`Exception setting torque for servo ${servoId}: ${err.message}`);
756
- }
757
- }
758
-
759
- // =============================================================================
760
- // SYNC WRITE OPERATIONS
761
- // =============================================================================
762
-
763
- /**
764
- * Writes target positions to multiple servos synchronously.
765
- * @param {Map<number, number> | object} servoPositions - A Map or object where keys are servo IDs (1-252) and values are target positions (0-4095).
766
- * @returns {Promise<"success">} Resolves with "success".
767
- * @throws {Error} If not connected, any position is out of range, transmission fails, or an exception occurs.
768
- */
769
- export async function syncWritePositions(servoPositions) {
770
- checkConnection();
771
-
772
- const groupSyncWrite = new GroupSyncWrite(portHandler, packetHandler, ADDR_SCS_GOAL_POSITION, 2);
773
- let paramAdded = false;
774
-
775
- const entries = servoPositions instanceof Map ? servoPositions.entries() : Object.entries(servoPositions);
776
-
777
- for (const [idStr, position] of entries) {
778
- const servoId = parseInt(idStr, 10);
779
- if (isNaN(servoId) || servoId < 1 || servoId > 252) {
780
- throw new Error(`Invalid servo ID "${idStr}" in syncWritePositions.`);
781
- }
782
- if (position < 0 || position > 4095) {
783
- throw new Error(`Invalid position value ${position} for servo ${servoId} in syncWritePositions. Must be between 0 and 4095.`);
784
- }
785
- const targetPosition = Math.round(position);
786
- const data = [SCS_LOBYTE(targetPosition), SCS_HIBYTE(targetPosition)];
787
-
788
- if (groupSyncWrite.addParam(servoId, data)) {
789
- paramAdded = true;
790
- } else {
791
- console.warn(`Failed to add servo ${servoId} to sync write group (possibly duplicate).`);
792
- }
793
- }
794
-
795
- if (!paramAdded) {
796
- debugLog("Sync Write: No valid servo positions provided or added.");
797
- return "success";
798
- }
799
-
800
- try {
801
- const result = await groupSyncWrite.txPacket();
802
- if (result !== COMM_SUCCESS) {
803
- throw new Error(`Sync Write txPacket failed: ${packetHandler.getTxRxResult(result)}`);
804
- }
805
- return "success";
806
- } catch (err) {
807
- console.error("Exception during syncWritePositions:", err);
808
- throw new Error(`Sync Write failed: ${err.message}`);
809
- }
810
- }
811
-
812
- /**
813
- * Writes a target speed for a servo in wheel mode.
814
- * @param {number} servoId - The ID of the servo
815
- * @param {number} speed - The target speed value (-10000 to 10000). Negative values indicate reverse direction. 0 stops the wheel.
816
- * @returns {Promise<"success">} Resolves with "success".
817
- * @throws {Error} If not connected, either write fails, or an exception occurs.
818
- */
819
- export async function writeWheelSpeed(servoId, speed) {
820
- checkConnection();
821
- let unlocked = false;
822
- try {
823
- const clampedSpeed = Math.max(-10000, Math.min(10000, Math.round(speed)));
824
- let speedValue = Math.abs(clampedSpeed) & 0x7fff;
825
-
826
- if (clampedSpeed < 0) {
827
- speedValue |= 0x8000;
828
- }
829
-
830
- debugLog(`Temporarily unlocking servo ${servoId} for wheel speed write...`);
831
-
832
- // 1. Unlock servo configuration first
833
- const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0);
834
- if (resUnlock !== COMM_SUCCESS) {
835
- debugLog(`Warning: Failed to unlock servo ${servoId}, trying direct write anyway...`);
836
- } else {
837
- unlocked = true;
838
- }
839
-
840
- // 2. Write the speed
841
- const [result, error] = await packetHandler.write2ByteTxRx(portHandler, servoId, ADDR_SCS_GOAL_SPEED, speedValue);
842
- if (result !== COMM_SUCCESS) {
843
- throw new Error(`Error writing wheel speed to servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error: ${error}`);
844
- }
845
-
846
- // 3. Lock servo configuration back
847
- if (unlocked) {
848
- const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1);
849
- if (resLock !== COMM_SUCCESS) {
850
- console.warn(`Warning: Failed to re-lock servo ${servoId} after wheel speed write: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`);
851
- } else {
852
- unlocked = false;
853
- }
854
- }
855
-
856
- return "success";
857
- } catch (err) {
858
- console.error(`Exception writing wheel speed to servo ${servoId}:`, err);
859
- if (unlocked) {
860
- await tryLockServo(servoId);
861
- }
862
- throw new Error(`Exception writing wheel speed to servo ${servoId}: ${err.message}`);
863
- }
864
- }
865
-
866
- /**
867
- * Writes target speeds to multiple servos in wheel mode synchronously.
868
- * @param {Map<number, number> | object} servoSpeeds - A Map or object where keys are servo IDs (1-252) and values are target speeds (-10000 to 10000).
869
- * @returns {Promise<"success">} Resolves with "success".
870
- * @throws {Error} If not connected, any speed is out of range, transmission fails, or an exception occurs.
871
- */
872
- export async function syncWriteWheelSpeed(servoSpeeds) {
873
- checkConnection();
874
-
875
- const groupSyncWrite = new GroupSyncWrite(
876
- portHandler,
877
- packetHandler,
878
- ADDR_SCS_GOAL_SPEED,
879
- 2 // Data length for speed (2 bytes)
880
- );
881
- let paramAdded = false;
882
-
883
- const entries = servoSpeeds instanceof Map ? servoSpeeds.entries() : Object.entries(servoSpeeds);
884
-
885
- // Second pass: Add valid parameters
886
- for (const [idStr, speed] of entries) {
887
- const servoId = parseInt(idStr, 10); // Already validated
888
-
889
- if (isNaN(servoId) || servoId < 1 || servoId > 252) {
890
- throw new Error(`Invalid servo ID "${idStr}" in syncWriteWheelSpeed.`);
891
- }
892
- if (speed < -10000 || speed > 10000) {
893
- throw new Error(
894
- `Invalid speed value ${speed} for servo ${servoId} in syncWriteWheelSpeed. Must be between -10000 and 10000.`
895
- );
896
- }
897
-
898
- const clampedSpeed = Math.max(-10000, Math.min(10000, Math.round(speed))); // Ensure integer, already validated range
899
- let speedValue = Math.abs(clampedSpeed) & 0x7fff; // Get absolute value, ensure within 15 bits
900
-
901
- // Set the direction bit (MSB of the 16-bit value) if speed is negative
902
- if (clampedSpeed < 0) {
903
- speedValue |= 0x8000; // Set the 16th bit for reverse direction
904
- }
905
-
906
- const data = [SCS_LOBYTE(speedValue), SCS_HIBYTE(speedValue)];
907
-
908
- if (groupSyncWrite.addParam(servoId, data)) {
909
- paramAdded = true;
910
- } else {
911
- // This should ideally not happen if IDs are unique, but handle defensively
912
- console.warn(
913
- `Failed to add servo ${servoId} to sync write speed group (possibly duplicate).`
914
- );
915
- }
916
- }
917
-
918
- if (!paramAdded) {
919
- debugLog("Sync Write Speed: No valid servo speeds provided or added.");
920
- return "success"; // Nothing to write is considered success
921
- }
922
-
923
- try {
924
- // Send the Sync Write instruction
925
- const result = await groupSyncWrite.txPacket();
926
- if (result !== COMM_SUCCESS) {
927
- throw new Error(`Sync Write Speed txPacket failed: ${packetHandler.getTxRxResult(result)}`);
928
- }
929
- return "success";
930
- } catch (err) {
931
- console.error("Exception during syncWriteWheelSpeed:", err);
932
- // Re-throw the original error or a new one wrapping it
933
- throw new Error(`Sync Write Speed failed: ${err.message}`);
934
- }
935
- }
936
-
937
- /**
938
- * Sets the Baud Rate of a servo.
939
- * NOTE: After changing the baud rate, you might need to disconnect and reconnect
940
- * at the new baud rate to communicate with the servo further.
941
- * @param {number} servoId - The current ID of the servo to configure (1-252).
942
- * @param {number} baudRateIndex - The index representing the new baud rate (0-7).
943
- * @returns {Promise<"success">} Resolves with "success".
944
- * @throws {Error} If not connected, input is invalid, any step fails, or an exception occurs.
945
- */
946
- export async function setBaudRate(servoId, baudRateIndex) {
947
- checkConnection();
948
-
949
- // Validate inputs
950
- if (servoId < 1 || servoId > 252) {
951
- throw new Error(`Invalid servo ID provided: ${servoId}. Must be between 1 and 252.`);
952
- }
953
- if (baudRateIndex < 0 || baudRateIndex > 7) {
954
- throw new Error(`Invalid baudRateIndex: ${baudRateIndex}. Must be between 0 and 7.`);
955
- }
956
-
957
- let unlocked = false;
958
- try {
959
- debugLog(`Setting baud rate for servo ${servoId}: Index=${baudRateIndex}`);
960
-
961
- // 1. Unlock servo configuration
962
- const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(
963
- portHandler,
964
- servoId,
965
- ADDR_SCS_LOCK,
966
- 0 // 0 to unlock
967
- );
968
- if (resUnlock !== COMM_SUCCESS) {
969
- throw new Error(
970
- `Failed to unlock servo ${servoId}: ${packetHandler.getTxRxResult(
971
- resUnlock
972
- )}, Error: ${errUnlock}`
973
- );
974
- }
975
- unlocked = true;
976
-
977
- // 2. Write new Baud Rate index
978
- const [resBaud, errBaud] = await packetHandler.write1ByteTxRx(
979
- portHandler,
980
- servoId,
981
- ADDR_SCS_BAUD_RATE,
982
- baudRateIndex
983
- );
984
- if (resBaud !== COMM_SUCCESS) {
985
- throw new Error(
986
- `Failed to write baud rate index ${baudRateIndex} to servo ${servoId}: ${packetHandler.getTxRxResult(
987
- resBaud
988
- )}, Error: ${errBaud}`
989
- );
990
- }
991
-
992
- // 3. Lock servo configuration
993
- const [resLock, errLock] = await packetHandler.write1ByteTxRx(
994
- portHandler,
995
- servoId,
996
- ADDR_SCS_LOCK,
997
- 1
998
- );
999
- if (resLock !== COMM_SUCCESS) {
1000
- throw new Error(
1001
- `Failed to lock servo ${servoId} after setting baud rate: ${packetHandler.getTxRxResult(
1002
- resLock
1003
- )}, Error: ${errLock}.`
1004
- );
1005
- }
1006
- unlocked = false; // Successfully locked
1007
-
1008
- debugLog(
1009
- `Successfully set baud rate for servo ${servoId}. Index: ${baudRateIndex}. Remember to potentially reconnect with the new baud rate.`
1010
- );
1011
- return "success";
1012
- } catch (err) {
1013
- console.error(`Exception during setBaudRate for servo ID ${servoId}:`, err);
1014
- if (unlocked) {
1015
- await tryLockServo(servoId);
1016
- }
1017
- throw new Error(`Failed to set baud rate for servo ${servoId}: ${err.message}`);
1018
- }
1019
- }
1020
-
1021
- /**
1022
- * Sets the ID of a servo.
1023
- * NOTE: Changing the ID requires using the new ID for subsequent commands.
1024
- * @param {number} currentServoId - The current ID of the servo to configure (1-252).
1025
- * @param {number} newServoId - The new ID to set for the servo (1-252).
1026
- * @returns {Promise<"success">} Resolves with "success".
1027
- * @throws {Error} If not connected, input is invalid, any step fails, or an exception occurs.
1028
- */
1029
- export async function setServoId(currentServoId, newServoId) {
1030
- checkConnection();
1031
-
1032
- // Validate inputs
1033
- if (currentServoId < 1 || currentServoId > 252 || newServoId < 1 || newServoId > 252) {
1034
- throw new Error(
1035
- `Invalid servo ID provided. Current: ${currentServoId}, New: ${newServoId}. Must be between 1 and 252.`
1036
- );
1037
- }
1038
-
1039
- if (currentServoId === newServoId) {
1040
- debugLog(`Servo ID is already ${newServoId}. No change needed.`);
1041
- return "success";
1042
- }
1043
-
1044
- let unlocked = false;
1045
- let idWritten = false;
1046
- try {
1047
- debugLog(`Setting servo ID: From ${currentServoId} to ${newServoId}`);
1048
-
1049
- // 1. Unlock servo configuration (using current ID)
1050
- const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(
1051
- portHandler,
1052
- currentServoId,
1053
- ADDR_SCS_LOCK,
1054
- 0 // 0 to unlock
1055
- );
1056
- if (resUnlock !== COMM_SUCCESS) {
1057
- throw new Error(
1058
- `Failed to unlock servo ${currentServoId}: ${packetHandler.getTxRxResult(
1059
- resUnlock
1060
- )}, Error: ${errUnlock}`
1061
- );
1062
- }
1063
- unlocked = true;
1064
-
1065
- // 2. Write new Servo ID (using current ID)
1066
- const [resId, errId] = await packetHandler.write1ByteTxRx(
1067
- portHandler,
1068
- currentServoId,
1069
- ADDR_SCS_ID,
1070
- newServoId
1071
- );
1072
- if (resId !== COMM_SUCCESS) {
1073
- throw new Error(
1074
- `Failed to write new ID ${newServoId} to servo ${currentServoId}: ${packetHandler.getTxRxResult(
1075
- resId
1076
- )}, Error: ${errId}`
1077
- );
1078
- }
1079
- idWritten = true;
1080
-
1081
- // 3. Lock servo configuration (using NEW ID)
1082
- const [resLock, errLock] = await packetHandler.write1ByteTxRx(
1083
- portHandler,
1084
- newServoId, // Use NEW ID here
1085
- ADDR_SCS_LOCK,
1086
- 1 // 1 to lock
1087
- );
1088
- if (resLock !== COMM_SUCCESS) {
1089
- // ID was likely changed, but lock failed. Critical state.
1090
- throw new Error(
1091
- `Failed to lock servo with new ID ${newServoId}: ${packetHandler.getTxRxResult(
1092
- resLock
1093
- )}, Error: ${errLock}. Configuration might be incomplete.`
1094
- );
1095
- }
1096
- unlocked = false; // Successfully locked with new ID
1097
-
1098
- debugLog(
1099
- `Successfully set servo ID from ${currentServoId} to ${newServoId}. Remember to use the new ID for future commands.`
1100
- );
1101
- return "success";
1102
- } catch (err) {
1103
- console.error(`Exception during setServoId for current ID ${currentServoId}:`, err);
1104
- if (unlocked) {
1105
- // If unlock succeeded but subsequent steps failed, attempt to re-lock.
1106
- // If ID write failed, use current ID. If ID write succeeded but lock failed, use new ID.
1107
- const idToLock = idWritten ? newServoId : currentServoId;
1108
- console.warn(`Attempting to re-lock servo using ID ${idToLock}...`);
1109
- await tryLockServo(idToLock);
1110
- }
1111
- throw new Error(
1112
- `Failed to set servo ID from ${currentServoId} to ${newServoId}: ${err.message}`
1113
- );
1114
- }
1115
- }
1116
-
1117
- // =============================================================================
1118
- // LEGACY COMPATIBILITY FUNCTIONS (for backward compatibility)
1119
- // =============================================================================
1120
-
1121
- /**
1122
- * Sets a servo to wheel mode (continuous rotation) with unlocking.
1123
- * @param {number} servoId - The ID of the servo (1-252).
1124
- * @returns {Promise<"success">} Resolves with "success".
1125
- * @throws {Error} If not connected, any step fails, or an exception occurs.
1126
- */
1127
- export async function setWheelMode(servoId) {
1128
- checkConnection();
1129
- let unlocked = false;
1130
- try {
1131
- debugLog(`Setting servo ${servoId} to wheel mode...`);
1132
-
1133
- // 1. Unlock servo configuration
1134
- const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0);
1135
- if (resUnlock !== COMM_SUCCESS) {
1136
- throw new Error(`Failed to unlock servo ${servoId}: ${packetHandler.getTxRxResult(resUnlock)}, Error: ${errUnlock}`);
1137
- }
1138
- unlocked = true;
1139
-
1140
- // 2. Set mode to 1 (Wheel/Speed mode)
1141
- const [resMode, errMode] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_MODE, 1);
1142
- if (resMode !== COMM_SUCCESS) {
1143
- throw new Error(`Failed to set wheel mode for servo ${servoId}: ${packetHandler.getTxRxResult(resMode)}, Error: ${errMode}`);
1144
- }
1145
-
1146
- // 3. Lock servo configuration
1147
- const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1);
1148
- if (resLock !== COMM_SUCCESS) {
1149
- throw new Error(`Failed to lock servo ${servoId} after setting mode: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`);
1150
- }
1151
- unlocked = false;
1152
-
1153
- debugLog(`Successfully set servo ${servoId} to wheel mode.`);
1154
- return "success";
1155
- } catch (err) {
1156
- console.error(`Exception setting wheel mode for servo ${servoId}:`, err);
1157
- if (unlocked) {
1158
- await tryLockServo(servoId);
1159
- }
1160
- throw new Error(`Failed to set wheel mode for servo ${servoId}: ${err.message}`);
1161
- }
1162
- }
1163
-
1164
- /**
1165
- * Sets a servo back to position control mode from wheel mode.
1166
- * @param {number} servoId - The ID of the servo (1-252).
1167
- * @returns {Promise<"success">} Resolves with "success".
1168
- * @throws {Error} If not connected, any step fails, or an exception occurs.
1169
- */
1170
- export async function setPositionMode(servoId) {
1171
- checkConnection();
1172
- let unlocked = false;
1173
- try {
1174
- debugLog(`Setting servo ${servoId} back to position mode...`);
1175
-
1176
- // 1. Unlock servo configuration
1177
- const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0);
1178
- if (resUnlock !== COMM_SUCCESS) {
1179
- throw new Error(`Failed to unlock servo ${servoId}: ${packetHandler.getTxRxResult(resUnlock)}, Error: ${errUnlock}`);
1180
- }
1181
- unlocked = true;
1182
-
1183
- // 2. Set mode to 0 (Position/Servo mode)
1184
- const [resMode, errMode] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_MODE, 0);
1185
- if (resMode !== COMM_SUCCESS) {
1186
- throw new Error(`Failed to set position mode for servo ${servoId}: ${packetHandler.getTxRxResult(resMode)}, Error: ${errMode}`);
1187
- }
1188
-
1189
- // 3. Lock servo configuration
1190
- const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1);
1191
- if (resLock !== COMM_SUCCESS) {
1192
- throw new Error(`Failed to lock servo ${servoId} after setting mode: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`);
1193
- }
1194
- unlocked = false;
1195
-
1196
- debugLog(`Successfully set servo ${servoId} back to position mode.`);
1197
- return "success";
1198
- } catch (err) {
1199
- console.error(`Exception setting position mode for servo ${servoId}:`, err);
1200
- if (unlocked) {
1201
- await tryLockServo(servoId);
1202
- }
1203
- throw new Error(`Failed to set position mode for servo ${servoId}: ${err.message}`);
1204
- }
1205
- }
 
1
  import {
2
+ PortHandler,
3
+ PacketHandler,
4
+ COMM_SUCCESS,
5
+ COMM_RX_TIMEOUT,
6
+ COMM_RX_CORRUPT,
7
+ COMM_RX_FAIL,
8
+ COMM_TX_FAIL,
9
+ COMM_NOT_AVAILABLE,
10
+ SCS_LOBYTE,
11
+ SCS_HIBYTE,
12
+ SCS_MAKEWORD,
13
+ GroupSyncRead, // Import GroupSyncRead
14
+ GroupSyncWrite, // Import GroupSyncWrite
15
  } from "./lowLevelSDK.mjs";
16
 
17
  // Import address constants from the correct file
18
  import {
19
+ ADDR_SCS_PRESENT_POSITION,
20
+ ADDR_SCS_GOAL_POSITION,
21
+ ADDR_SCS_TORQUE_ENABLE,
22
+ ADDR_SCS_GOAL_ACC,
23
+ ADDR_SCS_GOAL_SPEED,
24
  } from "./scsservo_constants.mjs";
25
 
 
 
 
26
  // Define constants not present in scsservo_constants.mjs
27
  const ADDR_SCS_MODE = 33;
28
  const ADDR_SCS_LOCK = 55;
29
  const ADDR_SCS_ID = 5; // Address for Servo ID
30
  const ADDR_SCS_BAUD_RATE = 6; // Address for Baud Rate
31
 
32
+ // --- Class-based multi-instance implementation ---
33
+ export class ScsServoSDK {
34
+ constructor() {
35
+ this.portHandler = null;
36
+ this.packetHandler = null;
37
+ }
38
+
39
+ async connect(options = {}) {
40
+ if (this.portHandler && this.portHandler.isOpen) {
41
+ console.log("Already connected.");
42
+ return true;
43
+ }
44
+ const { baudRate = 1000000, protocolEnd = 0 } = options;
45
+ try {
46
+ this.portHandler = new PortHandler();
47
+ const portRequested = await this.portHandler.requestPort();
48
+ if (!portRequested) {
49
+ this.portHandler = null;
50
+ throw new Error("Failed to select a serial port.");
51
+ }
52
+ this.portHandler.setBaudRate(baudRate);
53
+ const portOpened = await this.portHandler.openPort();
54
+ if (!portOpened) {
55
+ await this.portHandler.closePort().catch(console.error);
56
+ this.portHandler = null;
57
+ throw new Error(`Failed to open port at baudrate ${baudRate}.`);
58
+ }
59
+ this.packetHandler = new PacketHandler(protocolEnd);
60
+ console.log(
61
+ `Connected to serial port at ${baudRate} baud, protocol end: ${protocolEnd}.`
62
+ );
63
+ return true;
64
+ } catch (err) {
65
+ console.error("Error during connection:", err);
66
+ if (this.portHandler) {
67
+ try {
68
+ await this.portHandler.closePort();
69
+ } catch (closeErr) {
70
+ console.error(
71
+ "Error closing port after connection failure:",
72
+ closeErr
73
+ );
74
+ }
75
+ }
76
+ this.portHandler = null;
77
+ this.packetHandler = null;
78
+ throw new Error(`Connection failed: ${err.message}`);
79
+ }
80
+ }
81
+
82
+ async disconnect() {
83
+ if (!this.portHandler || !this.portHandler.isOpen) {
84
+ console.log("Already disconnected.");
85
+ return true;
86
+ }
87
+ try {
88
+ await this.portHandler.closePort();
89
+ this.portHandler = null;
90
+ this.packetHandler = null;
91
+ console.log("Disconnected from serial port.");
92
+ return true;
93
+ } catch (err) {
94
+ console.error("Error during disconnection:", err);
95
+ this.portHandler = null;
96
+ this.packetHandler = null;
97
+ throw new Error(`Disconnection failed: ${err.message}`);
98
+ }
99
+ }
100
+
101
+ checkConnection() {
102
+ if (!this.portHandler || !this.packetHandler) {
103
+ throw new Error("Not connected. Call connect() first.");
104
+ }
105
+ }
106
+
107
+ async readPosition(servoId) {
108
+ this.checkConnection();
109
+ try {
110
+ const [position, result, error] = await this.packetHandler.read2ByteTxRx(
111
+ this.portHandler,
112
+ servoId,
113
+ ADDR_SCS_PRESENT_POSITION
114
+ );
115
+ if (result !== COMM_SUCCESS) {
116
+ throw new Error(
117
+ `Error reading position from servo ${servoId}: ${this.packetHandler.getTxRxResult(
118
+ result
119
+ )}, Error code: ${error}`
120
+ );
121
+ }
122
+ return position & 0xffff;
123
+ } catch (err) {
124
+ console.error(`Exception reading position from servo ${servoId}:`, err);
125
+ throw new Error(
126
+ `Exception reading position from servo ${servoId}: ${err.message}`
127
+ );
128
+ }
129
+ }
130
+
131
+ async readBaudRate(servoId) {
132
+ this.checkConnection();
133
+ try {
134
+ const [baudIndex, result, error] = await this.packetHandler.read1ByteTxRx(
135
+ this.portHandler,
136
+ servoId,
137
+ ADDR_SCS_BAUD_RATE
138
+ );
139
+ if (result !== COMM_SUCCESS) {
140
+ throw new Error(
141
+ `Error reading baud rate from servo ${servoId}: ${this.packetHandler.getTxRxResult(
142
+ result
143
+ )}, Error code: ${error}`
144
+ );
145
+ }
146
+ return baudIndex;
147
+ } catch (err) {
148
+ console.error(`Exception reading baud rate from servo ${servoId}:`, err);
149
+ throw new Error(
150
+ `Exception reading baud rate from servo ${servoId}: ${err.message}`
151
+ );
152
+ }
153
+ }
154
+
155
+ async readMode(servoId) {
156
+ this.checkConnection();
157
+ try {
158
+ const [modeValue, result, error] = await this.packetHandler.read1ByteTxRx(
159
+ this.portHandler,
160
+ servoId,
161
+ ADDR_SCS_MODE
162
+ );
163
+ if (result !== COMM_SUCCESS) {
164
+ throw new Error(
165
+ `Error reading mode from servo ${servoId}: ${this.packetHandler.getTxRxResult(
166
+ result
167
+ )}, Error code: ${error}`
168
+ );
169
+ }
170
+ return modeValue;
171
+ } catch (err) {
172
+ console.error(`Exception reading mode from servo ${servoId}:`, err);
173
+ throw new Error(
174
+ `Exception reading mode from servo ${servoId}: ${err.message}`
175
+ );
176
+ }
177
+ }
178
+
179
+ async writePosition(servoId, position) {
180
+ this.checkConnection();
181
+ try {
182
+ if (position < 0 || position > 4095) {
183
+ throw new Error(
184
+ `Invalid position value ${position} for servo ${servoId}. Must be between 0 and 4095.`
185
+ );
186
+ }
187
+ const targetPosition = Math.round(position);
188
+ const [result, error] = await this.packetHandler.write2ByteTxRx(
189
+ this.portHandler,
190
+ servoId,
191
+ ADDR_SCS_GOAL_POSITION,
192
+ targetPosition
193
+ );
194
+ if (result !== COMM_SUCCESS) {
195
+ throw new Error(
196
+ `Error writing position to servo ${servoId}: ${this.packetHandler.getTxRxResult(
197
+ result
198
+ )}, Error code: ${error}`
199
+ );
200
+ }
201
+ return "success";
202
+ } catch (err) {
203
+ console.error(`Exception writing position to servo ${servoId}:`, err);
204
+ throw new Error(
205
+ `Failed to write position to servo ${servoId}: ${err.message}`
206
+ );
207
+ }
208
+ }
209
+
210
+ async writeTorqueEnable(servoId, enable) {
211
+ this.checkConnection();
212
+ try {
213
+ const enableValue = enable ? 1 : 0;
214
+ const [result, error] = await this.packetHandler.write1ByteTxRx(
215
+ this.portHandler,
216
+ servoId,
217
+ ADDR_SCS_TORQUE_ENABLE,
218
+ enableValue
219
+ );
220
+ if (result !== COMM_SUCCESS) {
221
+ throw new Error(
222
+ `Error setting torque for servo ${servoId}: ${this.packetHandler.getTxRxResult(
223
+ result
224
+ )}, Error code: ${error}`
225
+ );
226
+ }
227
+ return "success";
228
+ } catch (err) {
229
+ console.error(`Exception setting torque for servo ${servoId}:`, err);
230
+ throw new Error(
231
+ `Exception setting torque for servo ${servoId}: ${err.message}`
232
+ );
233
+ }
234
+ }
235
+
236
+ async writeAcceleration(servoId, acceleration) {
237
+ this.checkConnection();
238
+ try {
239
+ const clampedAcceleration = Math.max(
240
+ 0,
241
+ Math.min(254, Math.round(acceleration))
242
+ );
243
+ const [result, error] = await this.packetHandler.write1ByteTxRx(
244
+ this.portHandler,
245
+ servoId,
246
+ ADDR_SCS_GOAL_ACC,
247
+ clampedAcceleration
248
+ );
249
+ if (result !== COMM_SUCCESS) {
250
+ throw new Error(
251
+ `Error writing acceleration to servo ${servoId}: ${this.packetHandler.getTxRxResult(
252
+ result
253
+ )}, Error code: ${error}`
254
+ );
255
+ }
256
+ return "success";
257
+ } catch (err) {
258
+ console.error(`Exception writing acceleration to servo ${servoId}:`, err);
259
+ throw new Error(
260
+ `Exception writing acceleration to servo ${servoId}: ${err.message}`
261
+ );
262
+ }
263
+ }
264
+
265
+ async setWheelMode(servoId) {
266
+ this.checkConnection();
267
+ let unlocked = false;
268
+ try {
269
+ console.log(`Setting servo ${servoId} to wheel mode...`);
270
+ const [resUnlock, errUnlock] = await this.packetHandler.write1ByteTxRx(
271
+ this.portHandler,
272
+ servoId,
273
+ ADDR_SCS_LOCK,
274
+ 0
275
+ );
276
+ if (resUnlock !== COMM_SUCCESS) {
277
+ throw new Error(
278
+ `Failed to unlock servo ${servoId}: ${this.packetHandler.getTxRxResult(
279
+ resUnlock
280
+ )}, Error: ${errUnlock}`
281
+ );
282
+ }
283
+ unlocked = true;
284
+ const [resMode, errMode] = await this.packetHandler.write1ByteTxRx(
285
+ this.portHandler,
286
+ servoId,
287
+ ADDR_SCS_MODE,
288
+ 1
289
+ );
290
+ if (resMode !== COMM_SUCCESS) {
291
+ throw new Error(
292
+ `Failed to set wheel mode for servo ${servoId}: ${this.packetHandler.getTxRxResult(
293
+ resMode
294
+ )}, Error: ${errMode}`
295
+ );
296
+ }
297
+ const [resLock, errLock] = await this.packetHandler.write1ByteTxRx(
298
+ this.portHandler,
299
+ servoId,
300
+ ADDR_SCS_LOCK,
301
+ 1
302
+ );
303
+ if (resLock !== COMM_SUCCESS) {
304
+ throw new Error(
305
+ `Failed to lock servo ${servoId} after setting mode: ${this.packetHandler.getTxRxResult(
306
+ resLock
307
+ )}, Error: ${errLock}`
308
+ );
309
+ }
310
+ unlocked = false;
311
+ console.log(`Successfully set servo ${servoId} to wheel mode.`);
312
+ return "success";
313
+ } catch (err) {
314
+ console.error(`Exception setting wheel mode for servo ${servoId}:`, err);
315
+ if (unlocked) {
316
+ await this.tryLockServo(servoId);
317
+ }
318
+ throw new Error(
319
+ `Failed to set wheel mode for servo ${servoId}: ${err.message}`
320
+ );
321
+ }
322
+ }
323
+
324
+ async setPositionMode(servoId) {
325
+ this.checkConnection();
326
+ let unlocked = false;
327
+ try {
328
+ console.log(`Setting servo ${servoId} back to position mode...`);
329
+ const [resUnlock, errUnlock] = await this.packetHandler.write1ByteTxRx(
330
+ this.portHandler,
331
+ servoId,
332
+ ADDR_SCS_LOCK,
333
+ 0
334
+ );
335
+ if (resUnlock !== COMM_SUCCESS) {
336
+ throw new Error(
337
+ `Failed to unlock servo ${servoId}: ${this.packetHandler.getTxRxResult(
338
+ resUnlock
339
+ )}, Error: ${errUnlock}`
340
+ );
341
+ }
342
+ unlocked = true;
343
+ const [resMode, errMode] = await this.packetHandler.write1ByteTxRx(
344
+ this.portHandler,
345
+ servoId,
346
+ ADDR_SCS_MODE,
347
+ 0
348
+ );
349
+ if (resMode !== COMM_SUCCESS) {
350
+ throw new Error(
351
+ `Failed to set position mode for servo ${servoId}: ${this.packetHandler.getTxRxResult(
352
+ resMode
353
+ )}, Error: ${errMode}`
354
+ );
355
+ }
356
+ const [resLock, errLock] = await this.packetHandler.write1ByteTxRx(
357
+ this.portHandler,
358
+ servoId,
359
+ ADDR_SCS_LOCK,
360
+ 1
361
+ );
362
+ if (resLock !== COMM_SUCCESS) {
363
+ throw new Error(
364
+ `Failed to lock servo ${servoId} after setting mode: ${this.packetHandler.getTxRxResult(
365
+ resLock
366
+ )}, Error: ${errLock}`
367
+ );
368
+ }
369
+ unlocked = false;
370
+ console.log(`Successfully set servo ${servoId} back to position mode.`);
371
+ return "success";
372
+ } catch (err) {
373
+ console.error(
374
+ `Exception setting position mode for servo ${servoId}:`,
375
+ err
376
+ );
377
+ if (unlocked) {
378
+ await this.tryLockServo(servoId);
379
+ }
380
+ throw new Error(
381
+ `Failed to set position mode for servo ${servoId}: ${err.message}`
382
+ );
383
+ }
384
+ }
385
+
386
+ async tryLockServo(servoId) {
387
+ try {
388
+ await this.packetHandler.write1ByteTxRx(
389
+ this.portHandler,
390
+ servoId,
391
+ ADDR_SCS_LOCK,
392
+ 1
393
+ );
394
+ } catch (lockErr) {
395
+ console.error(`Failed to re-lock servo ${servoId}:`, lockErr);
396
+ }
397
+ }
398
+
399
+ async writeWheelSpeed(servoId, speed) {
400
+ this.checkConnection();
401
+ try {
402
+ const clampedSpeed = Math.max(-10000, Math.min(10000, Math.round(speed)));
403
+ let speedValue = Math.abs(clampedSpeed) & 0x7fff;
404
+ if (clampedSpeed < 0) {
405
+ speedValue |= 0x8000;
406
+ }
407
+ const [result, error] = await this.packetHandler.write2ByteTxRx(
408
+ this.portHandler,
409
+ servoId,
410
+ ADDR_SCS_GOAL_SPEED,
411
+ speedValue
412
+ );
413
+ if (result !== COMM_SUCCESS) {
414
+ throw new Error(
415
+ `Error writing wheel speed to servo ${servoId}: ${this.packetHandler.getTxRxResult(
416
+ result
417
+ )}, Error: ${error}`
418
+ );
419
+ }
420
+ return "success";
421
+ } catch (err) {
422
+ console.error(`Exception writing wheel speed to servo ${servoId}:`, err);
423
+ throw new Error(
424
+ `Exception writing wheel speed to servo ${servoId}: ${err.message}`
425
+ );
426
+ }
427
+ }
428
+
429
+ async syncWriteWheelSpeed(servoSpeeds) {
430
+ this.checkConnection();
431
+ const groupSyncWrite = new GroupSyncWrite(
432
+ this.portHandler,
433
+ this.packetHandler,
434
+ ADDR_SCS_GOAL_SPEED,
435
+ 2
436
+ );
437
+ let paramAdded = false;
438
+ const entries =
439
+ servoSpeeds instanceof Map
440
+ ? servoSpeeds.entries()
441
+ : Object.entries(servoSpeeds);
442
+ for (const [idStr, speed] of entries) {
443
+ const servoId = parseInt(idStr, 10);
444
+ if (isNaN(servoId) || servoId < 1 || servoId > 252) {
445
+ throw new Error(`Invalid servo ID "${idStr}" in syncWriteWheelSpeed.`);
446
+ }
447
+ if (speed < -10000 || speed > 10000) {
448
+ throw new Error(
449
+ `Invalid speed value ${speed} for servo ${servoId} in syncWriteWheelSpeed. Must be between -10000 and 10000.`
450
+ );
451
+ }
452
+ const clampedSpeed = Math.max(-10000, Math.min(10000, Math.round(speed)));
453
+ let speedValue = Math.abs(clampedSpeed) & 0x7fff;
454
+ if (clampedSpeed < 0) {
455
+ speedValue |= 0x8000;
456
+ }
457
+ const data = [SCS_LOBYTE(speedValue), SCS_HIBYTE(speedValue)];
458
+ if (groupSyncWrite.addParam(servoId, data)) {
459
+ paramAdded = true;
460
+ } else {
461
+ console.warn(
462
+ `Failed to add servo ${servoId} to sync write speed group (possibly duplicate).`
463
+ );
464
+ }
465
+ }
466
+ if (!paramAdded) {
467
+ console.log("Sync Write Speed: No valid servo speeds provided or added.");
468
+ return "success";
469
+ }
470
+ try {
471
+ const result = await groupSyncWrite.txPacket();
472
+ if (result !== COMM_SUCCESS) {
473
+ throw new Error(
474
+ `Sync Write Speed txPacket failed: ${this.packetHandler.getTxRxResult(
475
+ result
476
+ )}`
477
+ );
478
+ }
479
+ return "success";
480
+ } catch (err) {
481
+ console.error("Exception during syncWriteWheelSpeed:", err);
482
+ throw new Error(`Sync Write Speed failed: ${err.message}`);
483
+ }
484
+ }
485
+
486
+ async syncReadPositions(servoIds) {
487
+ this.checkConnection();
488
+ if (!Array.isArray(servoIds) || servoIds.length === 0) {
489
+ console.log("Sync Read: No servo IDs provided.");
490
+ return new Map();
491
+ }
492
+ const startAddress = ADDR_SCS_PRESENT_POSITION;
493
+ const positions = new Map();
494
+ for (const id of servoIds) {
495
+ if (id < 1 || id > 252) {
496
+ console.warn(`Sync Read: Invalid servo ID ${id} skipped.`);
497
+ continue;
498
+ }
499
+ try {
500
+ const [pos, result, error] = await this.packetHandler.read2ByteTxRx(
501
+ this.portHandler,
502
+ id,
503
+ startAddress
504
+ );
505
+ if (result === COMM_SUCCESS) {
506
+ positions.set(id, pos & 0xffff);
507
+ } else {
508
+ console.warn(
509
+ `Sync Read: Failed to read position for servo ID ${id}: ${this.packetHandler.getTxRxResult(
510
+ result
511
+ )}, Error: ${error}`
512
+ );
513
+ }
514
+ } catch (e) {
515
+ console.warn(`Sync Read: Exception reading servo ID ${id}:`, e);
516
+ }
517
+ }
518
+ return positions;
519
+ }
520
+
521
+ async syncWritePositions(servoPositions) {
522
+ this.checkConnection();
523
+ const groupSyncWrite = new GroupSyncWrite(
524
+ this.portHandler,
525
+ this.packetHandler,
526
+ ADDR_SCS_GOAL_POSITION,
527
+ 2
528
+ );
529
+ let paramAdded = false;
530
+ const entries =
531
+ servoPositions instanceof Map
532
+ ? servoPositions.entries()
533
+ : Object.entries(servoPositions);
534
+ for (const [idStr, position] of entries) {
535
+ const servoId = parseInt(idStr, 10);
536
+ if (isNaN(servoId) || servoId < 1 || servoId > 252) {
537
+ throw new Error(`Invalid servo ID "${idStr}" in syncWritePositions.`);
538
+ }
539
+ if (position < 0 || position > 4095) {
540
+ throw new Error(
541
+ `Invalid position value ${position} for servo ${servoId} in syncWritePositions. Must be between 0 and 4095.`
542
+ );
543
+ }
544
+ const targetPosition = Math.round(position);
545
+ const data = [SCS_LOBYTE(targetPosition), SCS_HIBYTE(targetPosition)];
546
+ if (groupSyncWrite.addParam(servoId, data)) {
547
+ paramAdded = true;
548
+ } else {
549
+ console.warn(
550
+ `Failed to add servo ${servoId} to sync write group (possibly duplicate).`
551
+ );
552
+ }
553
+ }
554
+ if (!paramAdded) {
555
+ console.log("Sync Write: No valid servo positions provided or added.");
556
+ return "success";
557
+ }
558
+ try {
559
+ const result = await groupSyncWrite.txPacket();
560
+ if (result !== COMM_SUCCESS) {
561
+ throw new Error(
562
+ `Sync Write txPacket failed: ${this.packetHandler.getTxRxResult(
563
+ result
564
+ )}`
565
+ );
566
+ }
567
+ return "success";
568
+ } catch (err) {
569
+ console.error("Exception during syncWritePositions:", err);
570
+ throw new Error(`Sync Write failed: ${err.message}`);
571
+ }
572
+ }
573
+
574
+ async setBaudRate(servoId, baudRateIndex) {
575
+ this.checkConnection();
576
+ if (servoId < 1 || servoId > 252) {
577
+ throw new Error(
578
+ `Invalid servo ID provided: ${servoId}. Must be between 1 and 252.`
579
+ );
580
+ }
581
+ if (baudRateIndex < 0 || baudRateIndex > 7) {
582
+ throw new Error(
583
+ `Invalid baudRateIndex: ${baudRateIndex}. Must be between 0 and 7.`
584
+ );
585
+ }
586
+ let unlocked = false;
587
+ try {
588
+ console.log(
589
+ `Setting baud rate for servo ${servoId}: Index=${baudRateIndex}`
590
+ );
591
+ const [resUnlock, errUnlock] = await this.packetHandler.write1ByteTxRx(
592
+ this.portHandler,
593
+ servoId,
594
+ ADDR_SCS_LOCK,
595
+ 0
596
+ );
597
+ if (resUnlock !== COMM_SUCCESS) {
598
+ throw new Error(
599
+ `Failed to unlock servo ${servoId}: ${this.packetHandler.getTxRxResult(
600
+ resUnlock
601
+ )}, Error: ${errUnlock}`
602
+ );
603
+ }
604
+ unlocked = true;
605
+ const [resBaud, errBaud] = await this.packetHandler.write1ByteTxRx(
606
+ this.portHandler,
607
+ servoId,
608
+ ADDR_SCS_BAUD_RATE,
609
+ baudRateIndex
610
+ );
611
+ if (resBaud !== COMM_SUCCESS) {
612
+ throw new Error(
613
+ `Failed to write baud rate index ${baudRateIndex} to servo ${servoId}: ${this.packetHandler.getTxRxResult(
614
+ resBaud
615
+ )}, Error: ${errBaud}`
616
+ );
617
+ }
618
+ const [resLock, errLock] = await this.packetHandler.write1ByteTxRx(
619
+ this.portHandler,
620
+ servoId,
621
+ ADDR_SCS_LOCK,
622
+ 1
623
+ );
624
+ if (resLock !== COMM_SUCCESS) {
625
+ throw new Error(
626
+ `Failed to lock servo ${servoId} after setting baud rate: ${this.packetHandler.getTxRxResult(
627
+ resLock
628
+ )}, Error: ${errLock}.`
629
+ );
630
+ }
631
+ unlocked = false;
632
+ console.log(
633
+ `Successfully set baud rate for servo ${servoId}. Index: ${baudRateIndex}. Remember to potentially reconnect with the new baud rate.`
634
+ );
635
+ return "success";
636
+ } catch (err) {
637
+ console.error(
638
+ `Exception during setBaudRate for servo ID ${servoId}:`,
639
+ err
640
+ );
641
+ if (unlocked) {
642
+ await this.tryLockServo(servoId);
643
+ }
644
+ throw new Error(
645
+ `Failed to set baud rate for servo ${servoId}: ${err.message}`
646
+ );
647
+ }
648
+ }
649
+
650
+ async setServoId(currentServoId, newServoId) {
651
+ this.checkConnection();
652
+ if (
653
+ currentServoId < 1 ||
654
+ currentServoId > 252 ||
655
+ newServoId < 1 ||
656
+ newServoId > 252
657
+ ) {
658
+ throw new Error(
659
+ `Invalid servo ID provided. Current: ${currentServoId}, New: ${newServoId}. Must be between 1 and 252.`
660
+ );
661
+ }
662
+ if (currentServoId === newServoId) {
663
+ console.log(`Servo ID is already ${newServoId}. No change needed.`);
664
+ return "success";
665
+ }
666
+ let unlocked = false;
667
+ let idWritten = false;
668
+ try {
669
+ console.log(`Setting servo ID: From ${currentServoId} to ${newServoId}`);
670
+ const [resUnlock, errUnlock] = await this.packetHandler.write1ByteTxRx(
671
+ this.portHandler,
672
+ currentServoId,
673
+ ADDR_SCS_LOCK,
674
+ 0
675
+ );
676
+ if (resUnlock !== COMM_SUCCESS) {
677
+ throw new Error(
678
+ `Failed to unlock servo ${currentServoId}: ${this.packetHandler.getTxRxResult(
679
+ resUnlock
680
+ )}, Error: ${errUnlock}`
681
+ );
682
+ }
683
+ unlocked = true;
684
+ const [resId, errId] = await this.packetHandler.write1ByteTxRx(
685
+ this.portHandler,
686
+ currentServoId,
687
+ ADDR_SCS_ID,
688
+ newServoId
689
+ );
690
+ if (resId !== COMM_SUCCESS) {
691
+ throw new Error(
692
+ `Failed to write new ID ${newServoId} to servo ${currentServoId}: ${this.packetHandler.getTxRxResult(
693
+ resId
694
+ )}, Error: ${errId}`
695
+ );
696
+ }
697
+ idWritten = true;
698
+ const [resLock, errLock] = await this.packetHandler.write1ByteTxRx(
699
+ this.portHandler,
700
+ newServoId,
701
+ ADDR_SCS_LOCK,
702
+ 1
703
+ );
704
+ if (resLock !== COMM_SUCCESS) {
705
+ throw new Error(
706
+ `Failed to lock servo with new ID ${newServoId}: ${this.packetHandler.getTxRxResult(
707
+ resLock
708
+ )}, Error: ${errLock}. Configuration might be incomplete.`
709
+ );
710
+ }
711
+ unlocked = false;
712
+ console.log(
713
+ `Successfully set servo ID from ${currentServoId} to ${newServoId}. Remember to use the new ID for future commands.`
714
+ );
715
+ return "success";
716
+ } catch (err) {
717
+ console.error(
718
+ `Exception during setServoId for current ID ${currentServoId}:`,
719
+ err
720
+ );
721
+ if (unlocked) {
722
+ const idToLock = idWritten ? newServoId : currentServoId;
723
+ console.warn(`Attempting to re-lock servo using ID ${idToLock}...`);
724
+ await this.tryLockServo(idToLock);
725
+ }
726
+ throw new Error(
727
+ `Failed to set servo ID from ${currentServoId} to ${newServoId}: ${err.message}`
728
+ );
729
+ }
730
+ }
731
  }
732
 
733
+ // For backward compatibility: keep a singleton instance
734
+ export const scsServoSDK = new ScsServoSDK();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
packages/feetech.js/scsservo_constants.mjs CHANGED
@@ -1,8 +1,8 @@
1
  // Constants for FeetTech SCS servos
2
 
3
  // Constants
4
- export const BROADCAST_ID = 0xfe; // 254
5
- export const MAX_ID = 0xfc; // 252
6
 
7
  // Protocol instructions
8
  export const INST_PING = 1;
@@ -10,19 +10,19 @@ export const INST_READ = 2;
10
  export const INST_WRITE = 3;
11
  export const INST_REG_WRITE = 4;
12
  export const INST_ACTION = 5;
13
- export const INST_SYNC_WRITE = 131; // 0x83
14
- export const INST_SYNC_READ = 130; // 0x82
15
- export const INST_STATUS = 85; // 0x55, status packet instruction (0x55)
16
 
17
  // Communication results
18
- export const COMM_SUCCESS = 0; // tx or rx packet communication success
19
- export const COMM_PORT_BUSY = -1; // Port is busy (in use)
20
- export const COMM_TX_FAIL = -2; // Failed transmit instruction packet
21
- export const COMM_RX_FAIL = -3; // Failed get status packet
22
- export const COMM_TX_ERROR = -4; // Incorrect instruction packet
23
- export const COMM_RX_WAITING = -5; // Now receiving status packet
24
- export const COMM_RX_TIMEOUT = -6; // There is no status packet
25
- export const COMM_RX_CORRUPT = -7; // Incorrect status packet
26
  export const COMM_NOT_AVAILABLE = -9;
27
 
28
  // Packet constants
@@ -50,4 +50,4 @@ export const ADDR_SCS_TORQUE_ENABLE = 40;
50
  export const ADDR_SCS_GOAL_ACC = 41;
51
  export const ADDR_SCS_GOAL_POSITION = 42;
52
  export const ADDR_SCS_GOAL_SPEED = 46;
53
- export const ADDR_SCS_PRESENT_POSITION = 56;
 
1
  // Constants for FeetTech SCS servos
2
 
3
  // Constants
4
+ export const BROADCAST_ID = 0xFE; // 254
5
+ export const MAX_ID = 0xFC; // 252
6
 
7
  // Protocol instructions
8
  export const INST_PING = 1;
 
10
  export const INST_WRITE = 3;
11
  export const INST_REG_WRITE = 4;
12
  export const INST_ACTION = 5;
13
+ export const INST_SYNC_WRITE = 131; // 0x83
14
+ export const INST_SYNC_READ = 130; // 0x82
15
+ export const INST_STATUS = 85; // 0x55, status packet instruction (0x55)
16
 
17
  // Communication results
18
+ export const COMM_SUCCESS = 0; // tx or rx packet communication success
19
+ export const COMM_PORT_BUSY = -1; // Port is busy (in use)
20
+ export const COMM_TX_FAIL = -2; // Failed transmit instruction packet
21
+ export const COMM_RX_FAIL = -3; // Failed get status packet
22
+ export const COMM_TX_ERROR = -4; // Incorrect instruction packet
23
+ export const COMM_RX_WAITING = -5; // Now receiving status packet
24
+ export const COMM_RX_TIMEOUT = -6; // There is no status packet
25
+ export const COMM_RX_CORRUPT = -7; // Incorrect status packet
26
  export const COMM_NOT_AVAILABLE = -9;
27
 
28
  // Packet constants
 
50
  export const ADDR_SCS_GOAL_ACC = 41;
51
  export const ADDR_SCS_GOAL_POSITION = 42;
52
  export const ADDR_SCS_GOAL_SPEED = 46;
53
+ export const ADDR_SCS_PRESENT_POSITION = 56;
packages/feetech.js/test.html CHANGED
@@ -1,770 +1,620 @@
1
- <!doctype html>
2
  <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Feetech Servo Test</title>
7
- <style>
8
- body {
9
- font-family: sans-serif;
10
- line-height: 1.6;
11
- padding: 20px;
12
- }
13
- .container {
14
- max-width: 800px;
15
- margin: auto;
16
- }
17
- .section {
18
- border: 1px solid #ccc;
19
- padding: 15px;
20
- margin-bottom: 20px;
21
- border-radius: 5px;
22
- }
23
- h2 {
24
- margin-top: 0;
25
- }
26
- label {
27
- display: inline-block;
28
- min-width: 100px;
29
- margin-bottom: 5px;
30
- }
31
- input[type="number"],
32
- input[type="text"] {
33
- width: 100px;
34
- padding: 5px;
35
- margin-right: 10px;
36
- margin-bottom: 10px;
37
- }
38
- button {
39
- padding: 8px 15px;
40
- margin-right: 10px;
41
- cursor: pointer;
42
- }
43
- pre {
44
- background-color: #f4f4f4;
45
- padding: 10px;
46
- border: 1px solid #ddd;
47
- border-radius: 3px;
48
- white-space: pre-wrap;
49
- word-wrap: break-word;
50
- }
51
- .status {
52
- font-weight: bold;
53
- }
54
- .success {
55
- color: green;
56
- }
57
- .error {
58
- color: red;
59
- }
60
- .log-area {
61
- margin-top: 10px;
62
- }
63
- </style>
64
- </head>
65
- <body>
66
- <div class="container">
67
- <h1>Feetech Servo Test Page</h1>
68
-
69
- <details class="section">
70
- <summary>Key Concepts</summary>
71
- <p>Understanding these parameters is crucial for controlling Feetech servos:</p>
72
- <ul>
73
- <li>
74
- <strong>Mode:</strong> Determines the servo's primary function.
75
- <ul>
76
- <li>
77
- <code>Mode 0</code>: Position/Servo Mode. The servo moves to and holds a specific
78
- angular position.
79
- </li>
80
- <li>
81
- <code>Mode 1</code>: Wheel/Speed Mode. The servo rotates continuously at a specified
82
- speed and direction, like a motor.
83
- </li>
84
- </ul>
85
- Changing the mode requires unlocking, writing the mode value (0 or 1), and locking the
86
- configuration.
87
- </li>
88
- <li>
89
- <strong>Position:</strong> In Position Mode (Mode 0), this value represents the target
90
- or current angular position of the servo's output shaft.
91
- <ul>
92
- <li>
93
- Range: Typically <code>0</code> to <code>4095</code> (representing a 12-bit
94
- resolution).
95
- </li>
96
- <li>
97
- Meaning: Corresponds to the servo's rotational range (e.g., 0-360 degrees or 0-270
98
- degrees, depending on the specific servo model). <code>0</code> is one end of the
99
- range, <code>4095</code> is the other.
100
- </li>
101
- </ul>
102
- </li>
103
- <li>
104
- <strong>Speed (Wheel Mode):</strong> In Wheel Mode (Mode 1), this value controls the
105
- rotational speed and direction.
106
- <ul>
107
- <li>
108
- Range: Typically <code>-2500</code> to <code>+2500</code>. (Note: Some documentation
109
- might mention -1023 to +1023, but the SDK example uses a wider range).
110
- </li>
111
- <li>
112
- Meaning: <code>0</code> stops the wheel. Positive values rotate in one direction
113
- (e.g., clockwise), negative values rotate in the opposite direction (e.g.,
114
- counter-clockwise). The magnitude determines the speed (larger absolute value means
115
- faster rotation).
116
- </li>
117
- <li>Control Address: <code>ADDR_SCS_GOAL_SPEED</code> (Register 46/47).</li>
118
- </ul>
119
- </li>
120
- <li>
121
- <strong>Acceleration:</strong> Controls how quickly the servo changes speed to reach its
122
- target position (in Position Mode) or target speed (in Wheel Mode).
123
- <ul>
124
- <li>Range: Typically <code>0</code> to <code>254</code>.</li>
125
- <li>
126
- Meaning: Defines the rate of change of speed. The unit is 100 steps/s².
127
- <code>0</code> usually means instantaneous acceleration (or minimal delay). Higher
128
- values result in slower, smoother acceleration and deceleration. For example, a
129
- value of <code>10</code> means the speed changes by 10 * 100 = 1000 steps per
130
- second, per second. This helps reduce jerky movements and mechanical stress.
131
- </li>
132
- <li>Control Address: <code>ADDR_SCS_GOAL_ACC</code> (Register 41).</li>
133
- </ul>
134
- </li>
135
- <li>
136
- <strong>Baud Rate:</strong> The speed of communication between the controller and the
137
- servo. It must match on both ends. Servos often support multiple baud rates, selectable
138
- via an index:
139
- <ul>
140
- <li>Index 0: 1,000,000 bps</li>
141
- <li>Index 1: 500,000 bps</li>
142
- <li>Index 2: 250,000 bps</li>
143
- <li>Index 3: 128,000 bps</li>
144
- <li>Index 4: 115,200 bps</li>
145
- <li>Index 5: 76,800 bps</li>
146
- <li>Index 6: 57,600 bps</li>
147
- <li>Index 7: 38,400 bps</li>
148
- </ul>
149
- </li>
150
- </ul>
151
- </details>
152
-
153
- <div class="section">
154
- <h2>Connection</h2>
155
- <button id="connectBtn">Connect</button>
156
- <button id="disconnectBtn">Disconnect</button>
157
- <p>Status: <span id="connectionStatus" class="status error">Disconnected</span></p>
158
- <label for="baudRate">Baud Rate:</label>
159
- <input type="number" id="baudRate" value="1000000" />
160
- <label for="protocolEnd">Protocol End (0=STS/SMS, 1=SCS):</label>
161
- <input type="number" id="protocolEnd" value="0" min="0" max="1" />
162
- </div>
163
-
164
- <div class="section">
165
- <h2>Scan Servos</h2>
166
- <label for="scanStartId">Start ID:</label>
167
- <input type="number" id="scanStartId" value="1" min="1" max="252" />
168
- <label for="scanEndId">End ID:</label>
169
- <input type="number" id="scanEndId" value="15" min="1" max="252" />
170
- <button id="scanServosBtn">Scan</button>
171
- <p>Scan Results:</p>
172
- <pre id="scanResultsOutput" style="max-height: 200px; overflow-y: auto"></pre>
173
- <!-- Added element for results -->
174
- </div>
175
-
176
- <div class="section">
177
- <h2>Single Servo Control</h2>
178
- <label for="servoId">Servo ID:</label>
179
- <input type="number" id="servoId" value="1" min="1" max="252" /><br />
180
-
181
- <label for="idWrite">Change servo ID:</label>
182
- <input type="number" id="idWrite" value="1" min="1" max="252" />
183
- <button id="writeIdBtn">Write</button><br />
184
-
185
- <label for="baudRead">Read Baud Rate:</label>
186
- <button id="readBaudBtn">Read</button>
187
- <span id="readBaudResult"></span><br />
188
-
189
- <label for="baudWrite">Write Baud Rate Index:</label>
190
- <input type="number" id="baudWrite" value="6" min="0" max="7" />
191
- <!-- Assuming index 0-7 -->
192
- <button id="writeBaudBtn">Write</button><br />
193
-
194
- <label for="positionRead">Read Position:</label>
195
- <button id="readPosBtn">Read</button>
196
- <span id="readPosResult"></span><br />
197
-
198
- <label for="positionWrite">Write Position:</label>
199
- <input type="number" id="positionWrite" value="1000" min="0" max="4095" />
200
- <button id="writePosBtn">Write</button><br />
201
-
202
- <label for="torqueEnable">Torque:</label>
203
- <button id="torqueEnableBtn">Enable</button>
204
- <button id="torqueDisableBtn">Disable</button><br />
205
-
206
- <label for="accelerationWrite">Write Acceleration:</label>
207
- <input type="number" id="accelerationWrite" value="50" min="0" max="254" />
208
- <button id="writeAccBtn">Write</button><br />
209
-
210
- <label for="wheelMode">Wheel Mode:</label>
211
- <button id="setWheelModeBtn">Set Wheel Mode</button>
212
- <button id="removeWheelModeBtn">Set Position Mode</button><br />
213
-
214
- <label for="wheelSpeedWrite">Write Wheel Speed:</label>
215
- <input type="number" id="wheelSpeedWrite" value="0" min="-2500" max="2500" />
216
- <button id="writeWheelSpeedBtn">Write Speed</button>
217
- </div>
218
-
219
- <div class="section">
220
- <h2>Sync Operations</h2>
221
- <label for="syncReadIds">Sync Read IDs (csv):</label>
222
- <input type="text" id="syncReadIds" value="1,2,3" style="width: 150px" />
223
- <button id="syncReadBtn">Sync Read Positions</button><br />
224
-
225
- <label for="syncWriteData">Sync Write (id:pos,...):</label>
226
- <input type="text" id="syncWriteData" value="1:1500,2:2500" style="width: 200px" />
227
- <button id="syncWriteBtn">Sync Write Positions</button><br />
228
-
229
- <label for="syncWriteSpeedData">Sync Write Speed (id:speed,...):</label>
230
- <input type="text" id="syncWriteSpeedData" value="1:500,2:-1000" style="width: 200px" />
231
- <button id="syncWriteSpeedBtn">Sync Write Speeds</button>
232
- <!-- New Button -->
233
- </div>
234
-
235
- <div class="section">
236
- <h2>Log Output</h2>
237
- <pre id="logOutput"></pre>
238
- </div>
239
- </div>
240
-
241
- <script type="module">
242
- // Import the scsServoSDK object from index.mjs
243
- import { scsServoSDK } from "./index.mjs";
244
- // No longer need COMM_SUCCESS etc. here as errors are thrown
245
-
246
- const connectBtn = document.getElementById("connectBtn");
247
- const disconnectBtn = document.getElementById("disconnectBtn");
248
- const connectionStatus = document.getElementById("connectionStatus");
249
- const baudRateInput = document.getElementById("baudRate");
250
- const protocolEndInput = document.getElementById("protocolEnd");
251
-
252
- const servoIdInput = document.getElementById("servoId");
253
- const readIdBtn = document.getElementById("readIdBtn"); // New
254
- const readIdResult = document.getElementById("readIdResult"); // New
255
- const idWriteInput = document.getElementById("idWrite"); // New
256
- const writeIdBtn = document.getElementById("writeIdBtn"); // New
257
- const readBaudBtn = document.getElementById("readBaudBtn"); // New
258
- const readBaudResult = document.getElementById("readBaudResult"); // New
259
- const baudWriteInput = document.getElementById("baudWrite"); // New
260
- const writeBaudBtn = document.getElementById("writeBaudBtn"); // New
261
- const readPosBtn = document.getElementById("readPosBtn");
262
- const readPosResult = document.getElementById("readPosResult");
263
- const positionWriteInput = document.getElementById("positionWrite");
264
- const writePosBtn = document.getElementById("writePosBtn");
265
- const torqueEnableBtn = document.getElementById("torqueEnableBtn");
266
- const torqueDisableBtn = document.getElementById("torqueDisableBtn");
267
- const accelerationWriteInput = document.getElementById("accelerationWrite");
268
- const writeAccBtn = document.getElementById("writeAccBtn");
269
- const setWheelModeBtn = document.getElementById("setWheelModeBtn");
270
- const removeWheelModeBtn = document.getElementById("removeWheelModeBtn"); // Get reference to the new button
271
- const wheelSpeedWriteInput = document.getElementById("wheelSpeedWrite");
272
- const writeWheelSpeedBtn = document.getElementById("writeWheelSpeedBtn");
273
-
274
- const syncReadIdsInput = document.getElementById("syncReadIds");
275
- const syncReadBtn = document.getElementById("syncReadBtn");
276
- const syncWriteDataInput = document.getElementById("syncWriteData");
277
- const syncWriteBtn = document.getElementById("syncWriteBtn");
278
- const syncWriteSpeedDataInput = document.getElementById("syncWriteSpeedData"); // New Input
279
- const syncWriteSpeedBtn = document.getElementById("syncWriteSpeedBtn"); // New Button
280
- const scanServosBtn = document.getElementById("scanServosBtn"); // Get reference to the scan button
281
- const scanStartIdInput = document.getElementById("scanStartId"); // Get reference to start ID input
282
- const scanEndIdInput = document.getElementById("scanEndId"); // Get reference to end ID input
283
- const scanResultsOutput = document.getElementById("scanResultsOutput"); // Get reference to the new results area
284
-
285
- const logOutput = document.getElementById("logOutput");
286
-
287
- let isConnected = false;
288
-
289
- function log(message) {
290
- console.log(message);
291
- const timestamp = new Date().toLocaleTimeString();
292
- logOutput.textContent = `[${timestamp}] ${message}\n` + logOutput.textContent;
293
- // Limit log size
294
- const lines = logOutput.textContent.split("\n"); // Use '\n' instead of literal newline
295
- if (lines.length > 50) {
296
- logOutput.textContent = lines.slice(0, 50).join("\n"); // Use '\n' instead of literal newline
297
- }
298
- }
299
-
300
- function updateConnectionStatus(connected, message) {
301
- isConnected = connected;
302
- connectionStatus.textContent = message || (connected ? "Connected" : "Disconnected");
303
- connectionStatus.className = `status ${connected ? "success" : "error"}`;
304
- log(`Connection status: ${connectionStatus.textContent}`);
305
- }
306
-
307
- connectBtn.onclick = async () => {
308
- log("Attempting to connect...");
309
- try {
310
- const baudRate = parseInt(baudRateInput.value, 10);
311
- const protocolEnd = parseInt(protocolEndInput.value, 10);
312
- // Use scsServoSDK - throws on error
313
- await scsServoSDK.connect({ baudRate, protocolEnd });
314
- updateConnectionStatus(true, "Connected");
315
- } catch (err) {
316
- updateConnectionStatus(false, `Connection error: ${err.message}`);
317
- console.error(err);
318
- }
319
- };
320
-
321
- disconnectBtn.onclick = async () => {
322
- log("Attempting to disconnect...");
323
- try {
324
- // Use scsServoSDK - throws on error
325
- await scsServoSDK.disconnect();
326
- updateConnectionStatus(false, "Disconnected"); // Success means disconnected
327
- } catch (err) {
328
- // Assuming disconnect might fail if already disconnected or other issues
329
- updateConnectionStatus(false, `Disconnection error: ${err.message}`);
330
- console.error(err);
331
- }
332
- };
333
-
334
- writeIdBtn.onclick = async () => {
335
- // New handler
336
- if (!isConnected) {
337
- log("Error: Not connected");
338
- return;
339
- }
340
- const currentId = parseInt(servoIdInput.value, 10);
341
- const newId = parseInt(idWriteInput.value, 10);
342
- if (isNaN(newId) || newId < 1 || newId > 252) {
343
- log(`Error: Invalid new ID ${newId}. Must be between 1 and 252.`);
344
- return;
345
- }
346
- log(`Writing new ID ${newId} to servo ${currentId}...`);
347
- try {
348
- // Use scsServoSDK - throws on error
349
- await scsServoSDK.setServoId(currentId, newId);
350
- log(`Successfully wrote new ID ${newId} to servo (was ${currentId}).`);
351
- // IMPORTANT: Update the main ID input to reflect the change
352
- servoIdInput.value = newId;
353
- log(`Servo ID input field updated to ${newId}.`);
354
- } catch (err) {
355
- log(`Error writing ID for servo ${currentId}: ${err.message}`);
356
- console.error(err);
357
- }
358
- };
359
-
360
- readBaudBtn.onclick = async () => {
361
- // New handler
362
- if (!isConnected) {
363
- log("Error: Not connected");
364
- return;
365
- }
366
- const id = parseInt(servoIdInput.value, 10);
367
- log(`Reading Baud Rate Index for servo ${id}...`);
368
- readBaudResult.textContent = "Reading...";
369
- try {
370
- // Use scsServoSDK - returns value directly or throws
371
- const baudRateIndex = await scsServoSDK.readBaudRate(id);
372
- readBaudResult.textContent = `Baud Index: ${baudRateIndex}`;
373
- log(`Servo ${id} Baud Rate Index: ${baudRateIndex}`);
374
- } catch (err) {
375
- readBaudResult.textContent = `Error: ${err.message}`;
376
- log(`Error reading Baud Rate Index for servo ${id}: ${err.message}`);
377
- console.error(err);
378
- }
379
- };
380
-
381
- writeBaudBtn.onclick = async () => {
382
- // New handler
383
- if (!isConnected) {
384
- log("Error: Not connected");
385
- return;
386
- }
387
- const id = parseInt(servoIdInput.value, 10);
388
- const newBaudIndex = parseInt(baudWriteInput.value, 10);
389
- if (isNaN(newBaudIndex) || newBaudIndex < 0 || newBaudIndex > 7) {
390
- // Adjust max index if needed
391
- log(`Error: Invalid new Baud Rate Index ${newBaudIndex}. Check valid range.`);
392
- return;
393
- }
394
- log(`Writing new Baud Rate Index ${newBaudIndex} to servo ${id}...`);
395
- try {
396
- // Use scsServoSDK - throws on error
397
- await scsServoSDK.setBaudRate(id, newBaudIndex);
398
- log(`Successfully wrote new Baud Rate Index ${newBaudIndex} to servo ${id}.`);
399
- log(
400
- `IMPORTANT: You may need to disconnect and reconnect with the new baud rate if it differs from the current connection baud rate.`
401
- );
402
- } catch (err) {
403
- log(`Error writing Baud Rate Index for servo ${id}: ${err.message}`);
404
- console.error(err);
405
- }
406
- };
407
-
408
- readPosBtn.onclick = async () => {
409
- if (!isConnected) {
410
- log("Error: Not connected");
411
- return;
412
- }
413
- const id = parseInt(servoIdInput.value, 10);
414
- log(`Reading position for servo ${id}...`);
415
- readPosResult.textContent = "Reading...";
416
- try {
417
- // Use scsServoSDK - returns value directly or throws
418
- const position = await scsServoSDK.readPosition(id);
419
- readPosResult.textContent = `Position: ${position}`;
420
- log(`Servo ${id} position: ${position}`);
421
- } catch (err) {
422
- readPosResult.textContent = `Error: ${err.message}`;
423
- log(`Error reading position for servo ${id}: ${err.message}`);
424
- console.error(err);
425
- }
426
- };
427
-
428
- writePosBtn.onclick = async () => {
429
- if (!isConnected) {
430
- log("Error: Not connected");
431
- return;
432
- }
433
- const id = parseInt(servoIdInput.value, 10);
434
- const pos = parseInt(positionWriteInput.value, 10);
435
- log(`Writing position ${pos} to servo ${id}...`);
436
- try {
437
- // Use scsServoSDK - throws on error
438
- await scsServoSDK.writePosition(id, pos);
439
- log(`Successfully wrote position ${pos} to servo ${id}.`);
440
- } catch (err) {
441
- log(`Error writing position for servo ${id}: ${err.message}`);
442
- console.error(err);
443
- }
444
- };
445
-
446
- torqueEnableBtn.onclick = async () => {
447
- if (!isConnected) {
448
- log("Error: Not connected");
449
- return;
450
- }
451
- const id = parseInt(servoIdInput.value, 10);
452
- log(`Enabling torque for servo ${id}...`);
453
- try {
454
- // Use scsServoSDK - throws on error
455
- await scsServoSDK.writeTorqueEnable(id, true);
456
- log(`Successfully enabled torque for servo ${id}.`);
457
- } catch (err) {
458
- log(`Error enabling torque for servo ${id}: ${err.message}`);
459
- console.error(err);
460
- }
461
- };
462
-
463
- torqueDisableBtn.onclick = async () => {
464
- if (!isConnected) {
465
- log("Error: Not connected");
466
- return;
467
- }
468
- const id = parseInt(servoIdInput.value, 10);
469
- log(`Disabling torque for servo ${id}...`);
470
- try {
471
- // Use scsServoSDK - throws on error
472
- await scsServoSDK.writeTorqueEnable(id, false);
473
- log(`Successfully disabled torque for servo ${id}.`);
474
- } catch (err) {
475
- log(`Error disabling torque for servo ${id}: ${err.message}`);
476
- console.error(err);
477
- }
478
- };
479
-
480
- writeAccBtn.onclick = async () => {
481
- if (!isConnected) {
482
- log("Error: Not connected");
483
- return;
484
- }
485
- const id = parseInt(servoIdInput.value, 10);
486
- const acc = parseInt(accelerationWriteInput.value, 10);
487
- log(`Writing acceleration ${acc} to servo ${id}...`);
488
- try {
489
- // Use scsServoSDK - throws on error
490
- await scsServoSDK.writeAcceleration(id, acc);
491
- log(`Successfully wrote acceleration ${acc} to servo ${id}.`);
492
- } catch (err) {
493
- log(`Error writing acceleration for servo ${id}: ${err.message}`);
494
- console.error(err);
495
- }
496
- };
497
-
498
- setWheelModeBtn.onclick = async () => {
499
- if (!isConnected) {
500
- log("Error: Not connected");
501
- return;
502
- }
503
- const id = parseInt(servoIdInput.value, 10);
504
- log(`Setting servo ${id} to wheel mode...`);
505
- try {
506
- // Use scsServoSDK - throws on error
507
- await scsServoSDK.setWheelMode(id);
508
- log(`Successfully set servo ${id} to wheel mode.`);
509
- } catch (err) {
510
- log(`Error setting wheel mode for servo ${id}: ${err.message}`);
511
- console.error(err);
512
- }
513
- };
514
-
515
- // Add event listener for the new button
516
- removeWheelModeBtn.onclick = async () => {
517
- if (!isConnected) {
518
- log("Error: Not connected");
519
- return;
520
- }
521
- const id = parseInt(servoIdInput.value, 10);
522
- log(`Setting servo ${id} back to position mode...`);
523
- try {
524
- // Use scsServoSDK - throws on error
525
- await scsServoSDK.setPositionMode(id);
526
- log(`Successfully set servo ${id} back to position mode.`);
527
- } catch (err) {
528
- log(`Error setting position mode for servo ${id}: ${err.message}`);
529
- console.error(err);
530
- }
531
- };
532
-
533
- writeWheelSpeedBtn.onclick = async () => {
534
- if (!isConnected) {
535
- log("Error: Not connected");
536
- return;
537
- }
538
- const id = parseInt(servoIdInput.value, 10);
539
- const speed = parseInt(wheelSpeedWriteInput.value, 10);
540
- log(`Writing wheel speed ${speed} to servo ${id}...`);
541
- try {
542
- // Use scsServoSDK - throws on error
543
- await scsServoSDK.writeWheelSpeed(id, speed);
544
- log(`Successfully wrote wheel speed ${speed} to servo ${id}.`);
545
- } catch (err) {
546
- log(`Error writing wheel speed for servo ${id}: ${err.message}`);
547
- console.error(err);
548
- }
549
- };
550
-
551
- syncReadBtn.onclick = async () => {
552
- if (!isConnected) {
553
- log("Error: Not connected");
554
- return;
555
- }
556
- const idsString = syncReadIdsInput.value;
557
- const ids = idsString
558
- .split(",")
559
- .map((s) => parseInt(s.trim(), 10))
560
- .filter((id) => !isNaN(id) && id > 0 && id < 253);
561
- if (ids.length === 0) {
562
- log("Sync Read: No valid servo IDs provided.");
563
- return;
564
- }
565
- log(`Sync reading positions for servos: ${ids.join(", ")}...`);
566
- try {
567
- // Use scsServoSDK - returns Map or throws
568
- const positions = await scsServoSDK.syncReadPositions(ids);
569
- let logMsg = "Sync Read Successful:\n";
570
- positions.forEach((pos, id) => {
571
- logMsg += ` Servo ${id}: Position=${pos}\n`;
572
- });
573
- log(logMsg.trim());
574
- } catch (err) {
575
- log(`Sync Read Failed: ${err.message}`);
576
- console.error(err);
577
- }
578
- };
579
-
580
- syncWriteBtn.onclick = async () => {
581
- if (!isConnected) {
582
- log("Error: Not connected");
583
- return;
584
- }
585
- const dataString = syncWriteDataInput.value;
586
- const positionMap = new Map();
587
- const pairs = dataString.split(",");
588
- let validData = false;
589
-
590
- pairs.forEach((pair) => {
591
- const parts = pair.split(":");
592
- if (parts.length === 2) {
593
- const id = parseInt(parts[0].trim(), 10);
594
- const pos = parseInt(parts[1].trim(), 10);
595
- // Position validation (0-4095)
596
- if (!isNaN(id) && id > 0 && id < 253 && !isNaN(pos) && pos >= 0 && pos <= 4095) {
597
- positionMap.set(id, pos);
598
- validData = true;
599
- } else {
600
- log(
601
- `Sync Write Position: Invalid data pair "${pair}". ID (1-252), Pos (0-4095). Skipping.`
602
- );
603
- }
604
- } else {
605
- log(`Sync Write Position: Invalid format "${pair}". Skipping.`);
606
- }
607
- });
608
-
609
- if (!validData) {
610
- log("Sync Write Position: No valid servo position data provided.");
611
- return;
612
- }
613
-
614
- log(
615
- `Sync writing positions: ${Array.from(positionMap.entries())
616
- .map(([id, pos]) => `${id}:${pos}`)
617
- .join(", ")}...`
618
- );
619
- try {
620
- // Use scsServoSDK - throws on error
621
- await scsServoSDK.syncWritePositions(positionMap);
622
- log(`Sync write position command sent successfully.`);
623
- } catch (err) {
624
- log(`Sync Write Position Failed: ${err.message}`);
625
- console.error(err);
626
- }
627
- };
628
-
629
- // New handler for Sync Write Speed
630
- syncWriteSpeedBtn.onclick = async () => {
631
- if (!isConnected) {
632
- log("Error: Not connected");
633
- return;
634
- }
635
- const dataString = syncWriteSpeedDataInput.value;
636
- const speedMap = new Map();
637
- const pairs = dataString.split(",");
638
- let validData = false;
639
-
640
- pairs.forEach((pair) => {
641
- const parts = pair.split(":");
642
- if (parts.length === 2) {
643
- const id = parseInt(parts[0].trim(), 10);
644
- const speed = parseInt(parts[1].trim(), 10);
645
- // Speed validation (-10000 to 10000)
646
- if (
647
- !isNaN(id) &&
648
- id > 0 &&
649
- id < 253 &&
650
- !isNaN(speed) &&
651
- speed >= -10000 &&
652
- speed <= 10000
653
- ) {
654
- speedMap.set(id, speed);
655
- validData = true;
656
- } else {
657
- log(
658
- `Sync Write Speed: Invalid data pair "${pair}". ID (1-252), Speed (-10000 to 10000). Skipping.`
659
- );
660
- }
661
- } else {
662
- log(`Sync Write Speed: Invalid format "${pair}". Skipping.`);
663
- }
664
- });
665
-
666
- if (!validData) {
667
- log("Sync Write Speed: No valid servo speed data provided.");
668
- return;
669
- }
670
-
671
- log(
672
- `Sync writing speeds: ${Array.from(speedMap.entries())
673
- .map(([id, speed]) => `${id}:${speed}`)
674
- .join(", ")}...`
675
- );
676
- try {
677
- // Use scsServoSDK - throws on error
678
- await scsServoSDK.syncWriteWheelSpeed(speedMap);
679
- log(`Sync write speed command sent successfully.`);
680
- } catch (err) {
681
- log(`Sync Write Speed Failed: ${err.message}`);
682
- console.error(err);
683
- }
684
- };
685
-
686
- scanServosBtn.onclick = async () => {
687
- if (!isConnected) {
688
- log("Error: Not connected");
689
- return;
690
- }
691
-
692
- const startId = parseInt(scanStartIdInput.value, 10);
693
- const endId = parseInt(scanEndIdInput.value, 10);
694
-
695
- if (isNaN(startId) || isNaN(endId) || startId < 1 || endId > 252 || startId > endId) {
696
- const errorMsg =
697
- "Error: Invalid scan ID range. Please enter values between 1 and 252, with Start ID <= End ID.";
698
- log(errorMsg);
699
- scanResultsOutput.textContent = errorMsg; // Show error in results area too
700
- return;
701
- }
702
-
703
- const startMsg = `Starting servo scan (IDs ${startId}-${endId})...`;
704
- log(startMsg);
705
- scanResultsOutput.textContent = startMsg + "\n"; // Clear and start results area
706
- scanServosBtn.disabled = true; // Disable button during scan
707
-
708
- let foundCount = 0;
709
-
710
- for (let id = startId; id <= endId; id++) {
711
- let resultMsg = `Scanning ID ${id}... `;
712
- try {
713
- // Attempt to read position. If it succeeds, the servo exists.
714
- // If it throws, the servo likely doesn't exist or there's another issue.
715
- const position = await scsServoSDK.readPosition(id);
716
- foundCount++;
717
-
718
- // Servo found, now try to read mode and baud rate
719
- let mode = "ReadError";
720
- let baudRateIndex = "ReadError";
721
- try {
722
- mode = await scsServoSDK.readMode(id);
723
- } catch (modeErr) {
724
- log(` Servo ${id}: Error reading mode: ${modeErr.message}`);
725
- }
726
- try {
727
- baudRateIndex = await scsServoSDK.readBaudRate(id);
728
- } catch (baudErr) {
729
- log(` Servo ${id}: Error reading baud rate: ${baudErr.message}`);
730
- }
731
-
732
- resultMsg += `FOUND: Pos=${position}, Mode=${mode}, BaudIdx=${baudRateIndex}`;
733
- log(
734
- ` Servo ${id} FOUND: Position=${position}, Mode=${mode}, BaudIndex=${baudRateIndex}`
735
- );
736
- } catch (err) {
737
- // Check if the error message indicates a timeout or non-response, which is expected for non-existent IDs
738
- // This check might need refinement based on the exact error messages thrown by readPosition
739
- if (
740
- err.message.includes("timeout") ||
741
- err.message.includes("No response") ||
742
- err.message.includes("failed: RX")
743
- ) {
744
- resultMsg += `No response`;
745
- // log(` Servo ${id}: No response`); // Optional: reduce log noise
746
- } else {
747
- // Log other unexpected errors
748
- resultMsg += `Error: ${err.message}`;
749
- log(` Servo ${id}: Error during scan: ${err.message}`);
750
- console.error(`Error scanning servo ${id}:`, err);
751
- }
752
- }
753
- scanResultsOutput.textContent += resultMsg + "\n"; // Append result to the results area
754
- scanResultsOutput.scrollTop = scanResultsOutput.scrollHeight; // Auto-scroll
755
- // Optional small delay between scans if needed
756
- // await new Promise(resolve => setTimeout(resolve, 10));
757
- }
758
-
759
- const finishMsg = `Servo scan finished. Found ${foundCount} servo(s).`;
760
- log(finishMsg);
761
- scanResultsOutput.textContent += finishMsg + "\n"; // Add finish message to results area
762
- scanResultsOutput.scrollTop = scanResultsOutput.scrollHeight; // Auto-scroll
763
- scanServosBtn.disabled = false; // Re-enable button
764
- };
765
-
766
- // Initial log
767
- log("Test page loaded. Please connect to a servo controller.");
768
- </script>
769
- </body>
770
  </html>
 
1
+ <!DOCTYPE html>
2
  <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Feetech Servo Test</title>
7
+ <style>
8
+ body { font-family: sans-serif; line-height: 1.6; padding: 20px; }
9
+ .container { max-width: 800px; margin: auto; }
10
+ .section { border: 1px solid #ccc; padding: 15px; margin-bottom: 20px; border-radius: 5px; }
11
+ h2 { margin-top: 0; }
12
+ label { display: inline-block; min-width: 100px; margin-bottom: 5px; }
13
+ input[type="number"], input[type="text"] { width: 100px; padding: 5px; margin-right: 10px; margin-bottom: 10px; }
14
+ button { padding: 8px 15px; margin-right: 10px; cursor: pointer; }
15
+ pre { background-color: #f4f4f4; padding: 10px; border: 1px solid #ddd; border-radius: 3px; white-space: pre-wrap; word-wrap: break-word; }
16
+ .status { font-weight: bold; }
17
+ .success { color: green; }
18
+ .error { color: red; }
19
+ .log-area { margin-top: 10px; }
20
+ </style>
21
+ </head>
22
+ <body>
23
+ <div class="container">
24
+ <h1>Feetech Servo Test Page</h1>
25
+
26
+ <details class="section">
27
+ <summary>Key Concepts</summary>
28
+ <p>Understanding these parameters is crucial for controlling Feetech servos:</p>
29
+ <ul>
30
+ <li>
31
+ <strong>Mode:</strong> Determines the servo's primary function.
32
+ <ul>
33
+ <li><code>Mode 0</code>: Position/Servo Mode. The servo moves to and holds a specific angular position.</li>
34
+ <li><code>Mode 1</code>: Wheel/Speed Mode. The servo rotates continuously at a specified speed and direction, like a motor.</li>
35
+ </ul>
36
+ Changing the mode requires unlocking, writing the mode value (0 or 1), and locking the configuration.
37
+ </li>
38
+ <li>
39
+ <strong>Position:</strong> In Position Mode (Mode 0), this value represents the target or current angular position of the servo's output shaft.
40
+ <ul>
41
+ <li>Range: Typically <code>0</code> to <code>4095</code> (representing a 12-bit resolution).</li>
42
+ <li>Meaning: Corresponds to the servo's rotational range (e.g., 0-360 degrees or 0-270 degrees, depending on the specific servo model). <code>0</code> is one end of the range, <code>4095</code> is the other.</li>
43
+ </ul>
44
+ </li>
45
+ <li>
46
+ <strong>Speed (Wheel Mode):</strong> In Wheel Mode (Mode 1), this value controls the rotational speed and direction.
47
+ <ul>
48
+ <li>Range: Typically <code>-2500</code> to <code>+2500</code>. (Note: Some documentation might mention -1023 to +1023, but the SDK example uses a wider range).</li>
49
+ <li>Meaning: <code>0</code> stops the wheel. Positive values rotate in one direction (e.g., clockwise), negative values rotate in the opposite direction (e.g., counter-clockwise). The magnitude determines the speed (larger absolute value means faster rotation).</li>
50
+ <li>Control Address: <code>ADDR_SCS_GOAL_SPEED</code> (Register 46/47).</li>
51
+ </ul>
52
+ </li>
53
+ <li>
54
+ <strong>Acceleration:</strong> Controls how quickly the servo changes speed to reach its target position (in Position Mode) or target speed (in Wheel Mode).
55
+ <ul>
56
+ <li>Range: Typically <code>0</code> to <code>254</code>.</li>
57
+ <li>Meaning: Defines the rate of change of speed. The unit is 100 steps/s². <code>0</code> usually means instantaneous acceleration (or minimal delay). Higher values result in slower, smoother acceleration and deceleration. For example, a value of <code>10</code> means the speed changes by 10 * 100 = 1000 steps per second, per second. This helps reduce jerky movements and mechanical stress.</li>
58
+ <li>Control Address: <code>ADDR_SCS_GOAL_ACC</code> (Register 41).</li>
59
+ </ul>
60
+ </li>
61
+ <li>
62
+ <strong>Baud Rate:</strong> The speed of communication between the controller and the servo. It must match on both ends. Servos often support multiple baud rates, selectable via an index:
63
+ <ul>
64
+ <li>Index 0: 1,000,000 bps</li>
65
+ <li>Index 1: 500,000 bps</li>
66
+ <li>Index 2: 250,000 bps</li>
67
+ <li>Index 3: 128,000 bps</li>
68
+ <li>Index 4: 115,200 bps</li>
69
+ <li>Index 5: 76,800 bps</li>
70
+ <li>Index 6: 57,600 bps</li>
71
+ <li>Index 7: 38,400 bps</li>
72
+ </ul>
73
+ </li>
74
+ </ul>
75
+ </details>
76
+
77
+
78
+ <div class="section">
79
+ <h2>Connection</h2>
80
+ <button id="connectBtn">Connect</button>
81
+ <button id="disconnectBtn">Disconnect</button>
82
+ <p>Status: <span id="connectionStatus" class="status error">Disconnected</span></p>
83
+ <label for="baudRate">Baud Rate:</label>
84
+ <input type="number" id="baudRate" value="1000000">
85
+ <label for="protocolEnd">Protocol End (0=STS/SMS, 1=SCS):</label>
86
+ <input type="number" id="protocolEnd" value="0" min="0" max="1">
87
+ </div>
88
+
89
+ <div class="section">
90
+ <h2>Scan Servos</h2>
91
+ <label for="scanStartId">Start ID:</label>
92
+ <input type="number" id="scanStartId" value="1" min="1" max="252">
93
+ <label for="scanEndId">End ID:</label>
94
+ <input type="number" id="scanEndId" value="15" min="1" max="252">
95
+ <button id="scanServosBtn">Scan</button>
96
+ <p>Scan Results:</p>
97
+ <pre id="scanResultsOutput" style="max-height: 200px; overflow-y: auto;"></pre> <!-- Added element for results -->
98
+ </div>
99
+
100
+ <div class="section">
101
+ <h2>Single Servo Control</h2>
102
+ <label for="servoId">Servo ID:</label>
103
+ <input type="number" id="servoId" value="1" min="1" max="252"><br>
104
+
105
+
106
+ <label for="idWrite">Change servo ID:</label>
107
+ <input type="number" id="idWrite" value="1" min="1" max="252">
108
+ <button id="writeIdBtn">Write</button><br>
109
+
110
+ <label for="baudRead">Read Baud Rate:</label>
111
+ <button id="readBaudBtn">Read</button>
112
+ <span id="readBaudResult"></span><br>
113
+
114
+ <label for="baudWrite">Write Baud Rate Index:</label>
115
+ <input type="number" id="baudWrite" value="6" min="0" max="7"> <!-- Assuming index 0-7 -->
116
+ <button id="writeBaudBtn">Write</button><br>
117
+
118
+ <label for="positionRead">Read Position:</label>
119
+ <button id="readPosBtn">Read</button>
120
+ <span id="readPosResult"></span><br>
121
+
122
+ <label for="positionWrite">Write Position:</label>
123
+ <input type="number" id="positionWrite" value="1000" min="0" max="4095">
124
+ <button id="writePosBtn">Write</button><br>
125
+
126
+ <label for="torqueEnable">Torque:</label>
127
+ <button id="torqueEnableBtn">Enable</button>
128
+ <button id="torqueDisableBtn">Disable</button><br>
129
+
130
+ <label for="accelerationWrite">Write Acceleration:</label>
131
+ <input type="number" id="accelerationWrite" value="50" min="0" max="254">
132
+ <button id="writeAccBtn">Write</button><br>
133
+
134
+ <label for="wheelMode">Wheel Mode:</label>
135
+ <button id="setWheelModeBtn">Set Wheel Mode</button>
136
+ <button id="removeWheelModeBtn">Set Position Mode</button><br>
137
+
138
+ <label for="wheelSpeedWrite">Write Wheel Speed:</label>
139
+ <input type="number" id="wheelSpeedWrite" value="0" min="-2500" max="2500">
140
+ <button id="writeWheelSpeedBtn">Write Speed</button>
141
+ </div>
142
+
143
+ <div class="section">
144
+ <h2>Sync Operations</h2>
145
+ <label for="syncReadIds">Sync Read IDs (csv):</label>
146
+ <input type="text" id="syncReadIds" value="1,2,3" style="width: 150px;">
147
+ <button id="syncReadBtn">Sync Read Positions</button><br>
148
+
149
+ <label for="syncWriteData">Sync Write (id:pos,...):</label>
150
+ <input type="text" id="syncWriteData" value="1:1500,2:2500" style="width: 200px;">
151
+ <button id="syncWriteBtn">Sync Write Positions</button><br>
152
+
153
+ <label for="syncWriteSpeedData">Sync Write Speed (id:speed,...):</label>
154
+ <input type="text" id="syncWriteSpeedData" value="1:500,2:-1000" style="width: 200px;">
155
+ <button id="syncWriteSpeedBtn">Sync Write Speeds</button> <!-- New Button -->
156
+ </div>
157
+
158
+
159
+ <div class="section">
160
+ <h2>Log Output</h2>
161
+ <pre id="logOutput"></pre>
162
+ </div>
163
+ </div>
164
+
165
+ <script type="module">
166
+ // Import the scsServoSDK object from index.mjs
167
+ import { ScsServoSDK } from './index.mjs';
168
+ const scsServoSDK = new ScsServoSDK();
169
+ // No longer need COMM_SUCCESS etc. here as errors are thrown
170
+
171
+ const connectBtn = document.getElementById('connectBtn');
172
+ const disconnectBtn = document.getElementById('disconnectBtn');
173
+ const connectionStatus = document.getElementById('connectionStatus');
174
+ const baudRateInput = document.getElementById('baudRate');
175
+ const protocolEndInput = document.getElementById('protocolEnd');
176
+
177
+ const servoIdInput = document.getElementById('servoId');
178
+ const readIdBtn = document.getElementById('readIdBtn'); // New
179
+ const readIdResult = document.getElementById('readIdResult'); // New
180
+ const idWriteInput = document.getElementById('idWrite'); // New
181
+ const writeIdBtn = document.getElementById('writeIdBtn'); // New
182
+ const readBaudBtn = document.getElementById('readBaudBtn'); // New
183
+ const readBaudResult = document.getElementById('readBaudResult'); // New
184
+ const baudWriteInput = document.getElementById('baudWrite'); // New
185
+ const writeBaudBtn = document.getElementById('writeBaudBtn'); // New
186
+ const readPosBtn = document.getElementById('readPosBtn');
187
+ const readPosResult = document.getElementById('readPosResult');
188
+ const positionWriteInput = document.getElementById('positionWrite');
189
+ const writePosBtn = document.getElementById('writePosBtn');
190
+ const torqueEnableBtn = document.getElementById('torqueEnableBtn');
191
+ const torqueDisableBtn = document.getElementById('torqueDisableBtn');
192
+ const accelerationWriteInput = document.getElementById('accelerationWrite');
193
+ const writeAccBtn = document.getElementById('writeAccBtn');
194
+ const setWheelModeBtn = document.getElementById('setWheelModeBtn');
195
+ const removeWheelModeBtn = document.getElementById('removeWheelModeBtn'); // Get reference to the new button
196
+ const wheelSpeedWriteInput = document.getElementById('wheelSpeedWrite');
197
+ const writeWheelSpeedBtn = document.getElementById('writeWheelSpeedBtn');
198
+
199
+ const syncReadIdsInput = document.getElementById('syncReadIds');
200
+ const syncReadBtn = document.getElementById('syncReadBtn');
201
+ const syncWriteDataInput = document.getElementById('syncWriteData');
202
+ const syncWriteBtn = document.getElementById('syncWriteBtn');
203
+ const syncWriteSpeedDataInput = document.getElementById('syncWriteSpeedData'); // New Input
204
+ const syncWriteSpeedBtn = document.getElementById('syncWriteSpeedBtn'); // New Button
205
+ const scanServosBtn = document.getElementById('scanServosBtn'); // Get reference to the scan button
206
+ const scanStartIdInput = document.getElementById('scanStartId'); // Get reference to start ID input
207
+ const scanEndIdInput = document.getElementById('scanEndId'); // Get reference to end ID input
208
+ const scanResultsOutput = document.getElementById('scanResultsOutput'); // Get reference to the new results area
209
+
210
+ const logOutput = document.getElementById('logOutput');
211
+
212
+ let isConnected = false;
213
+
214
+ function log(message) {
215
+ console.log(message);
216
+ const timestamp = new Date().toLocaleTimeString();
217
+ logOutput.textContent = `[${timestamp}] ${message}\n` + logOutput.textContent;
218
+ // Limit log size
219
+ const lines = logOutput.textContent.split('\n'); // Use '\n' instead of literal newline
220
+ if (lines.length > 50) {
221
+ logOutput.textContent = lines.slice(0, 50).join('\n'); // Use '\n' instead of literal newline
222
+ }
223
+ }
224
+
225
+ function updateConnectionStatus(connected, message) {
226
+ isConnected = connected;
227
+ connectionStatus.textContent = message || (connected ? 'Connected' : 'Disconnected');
228
+ connectionStatus.className = `status ${connected ? 'success' : 'error'}`;
229
+ log(`Connection status: ${connectionStatus.textContent}`);
230
+ }
231
+
232
+ connectBtn.onclick = async () => {
233
+ log('Attempting to connect...');
234
+ try {
235
+ const baudRate = parseInt(baudRateInput.value, 10);
236
+ const protocolEnd = parseInt(protocolEndInput.value, 10);
237
+ // Use scsServoSDK - throws on error
238
+ await scsServoSDK.connect({ baudRate, protocolEnd });
239
+ updateConnectionStatus(true, 'Connected');
240
+ } catch (err) {
241
+ updateConnectionStatus(false, `Connection error: ${err.message}`);
242
+ console.error(err);
243
+ }
244
+ };
245
+
246
+ disconnectBtn.onclick = async () => {
247
+ log('Attempting to disconnect...');
248
+ try {
249
+ // Use scsServoSDK - throws on error
250
+ await scsServoSDK.disconnect();
251
+ updateConnectionStatus(false, 'Disconnected'); // Success means disconnected
252
+ } catch (err) {
253
+ // Assuming disconnect might fail if already disconnected or other issues
254
+ updateConnectionStatus(false, `Disconnection error: ${err.message}`);
255
+ console.error(err);
256
+ }
257
+ };
258
+
259
+ writeIdBtn.onclick = async () => { // New handler
260
+ if (!isConnected) { log('Error: Not connected'); return; }
261
+ const currentId = parseInt(servoIdInput.value, 10);
262
+ const newId = parseInt(idWriteInput.value, 10);
263
+ if (isNaN(newId) || newId < 1 || newId > 252) {
264
+ log(`Error: Invalid new ID ${newId}. Must be between 1 and 252.`);
265
+ return;
266
+ }
267
+ log(`Writing new ID ${newId} to servo ${currentId}...`);
268
+ try {
269
+ // Use scsServoSDK - throws on error
270
+ await scsServoSDK.setServoId(currentId, newId);
271
+ log(`Successfully wrote new ID ${newId} to servo (was ${currentId}).`);
272
+ // IMPORTANT: Update the main ID input to reflect the change
273
+ servoIdInput.value = newId;
274
+ log(`Servo ID input field updated to ${newId}.`);
275
+ } catch (err) {
276
+ log(`Error writing ID for servo ${currentId}: ${err.message}`);
277
+ console.error(err);
278
+ }
279
+ };
280
+
281
+ readBaudBtn.onclick = async () => { // New handler
282
+ if (!isConnected) { log('Error: Not connected'); return; }
283
+ const id = parseInt(servoIdInput.value, 10);
284
+ log(`Reading Baud Rate Index for servo ${id}...`);
285
+ readBaudResult.textContent = 'Reading...';
286
+ try {
287
+ // Use scsServoSDK - returns value directly or throws
288
+ const baudRateIndex = await scsServoSDK.readBaudRate(id);
289
+ readBaudResult.textContent = `Baud Index: ${baudRateIndex}`;
290
+ log(`Servo ${id} Baud Rate Index: ${baudRateIndex}`);
291
+ } catch (err) {
292
+ readBaudResult.textContent = `Error: ${err.message}`;
293
+ log(`Error reading Baud Rate Index for servo ${id}: ${err.message}`);
294
+ console.error(err);
295
+ }
296
+ };
297
+
298
+ writeBaudBtn.onclick = async () => { // New handler
299
+ if (!isConnected) { log('Error: Not connected'); return; }
300
+ const id = parseInt(servoIdInput.value, 10);
301
+ const newBaudIndex = parseInt(baudWriteInput.value, 10);
302
+ if (isNaN(newBaudIndex) || newBaudIndex < 0 || newBaudIndex > 7) { // Adjust max index if needed
303
+ log(`Error: Invalid new Baud Rate Index ${newBaudIndex}. Check valid range.`);
304
+ return;
305
+ }
306
+ log(`Writing new Baud Rate Index ${newBaudIndex} to servo ${id}...`);
307
+ try {
308
+ // Use scsServoSDK - throws on error
309
+ await scsServoSDK.setBaudRate(id, newBaudIndex);
310
+ log(`Successfully wrote new Baud Rate Index ${newBaudIndex} to servo ${id}.`);
311
+ log(`IMPORTANT: You may need to disconnect and reconnect with the new baud rate if it differs from the current connection baud rate.`);
312
+ } catch (err) {
313
+ log(`Error writing Baud Rate Index for servo ${id}: ${err.message}`);
314
+ console.error(err);
315
+ }
316
+ };
317
+
318
+
319
+ readPosBtn.onclick = async () => {
320
+ if (!isConnected) { log('Error: Not connected'); return; }
321
+ const id = parseInt(servoIdInput.value, 10);
322
+ log(`Reading position for servo ${id}...`);
323
+ readPosResult.textContent = 'Reading...';
324
+ try {
325
+ // Use scsServoSDK - returns value directly or throws
326
+ const position = await scsServoSDK.readPosition(id);
327
+ readPosResult.textContent = `Position: ${position}`;
328
+ log(`Servo ${id} position: ${position}`);
329
+ } catch (err) {
330
+ readPosResult.textContent = `Error: ${err.message}`;
331
+ log(`Error reading position for servo ${id}: ${err.message}`);
332
+ console.error(err);
333
+ }
334
+ };
335
+
336
+ writePosBtn.onclick = async () => {
337
+ if (!isConnected) { log('Error: Not connected'); return; }
338
+ const id = parseInt(servoIdInput.value, 10);
339
+ const pos = parseInt(positionWriteInput.value, 10);
340
+ log(`Writing position ${pos} to servo ${id}...`);
341
+ try {
342
+ // Use scsServoSDK - throws on error
343
+ await scsServoSDK.writePosition(id, pos);
344
+ log(`Successfully wrote position ${pos} to servo ${id}.`);
345
+ } catch (err) {
346
+ log(`Error writing position for servo ${id}: ${err.message}`);
347
+ console.error(err);
348
+ }
349
+ };
350
+
351
+ torqueEnableBtn.onclick = async () => {
352
+ if (!isConnected) { log('Error: Not connected'); return; }
353
+ const id = parseInt(servoIdInput.value, 10);
354
+ log(`Enabling torque for servo ${id}...`);
355
+ try {
356
+ // Use scsServoSDK - throws on error
357
+ await scsServoSDK.writeTorqueEnable(id, true);
358
+ log(`Successfully enabled torque for servo ${id}.`);
359
+ } catch (err) {
360
+ log(`Error enabling torque for servo ${id}: ${err.message}`);
361
+ console.error(err);
362
+ }
363
+ };
364
+
365
+ torqueDisableBtn.onclick = async () => {
366
+ if (!isConnected) { log('Error: Not connected'); return; }
367
+ const id = parseInt(servoIdInput.value, 10);
368
+ log(`Disabling torque for servo ${id}...`);
369
+ try {
370
+ // Use scsServoSDK - throws on error
371
+ await scsServoSDK.writeTorqueEnable(id, false);
372
+ log(`Successfully disabled torque for servo ${id}.`);
373
+ } catch (err) {
374
+ log(`Error disabling torque for servo ${id}: ${err.message}`);
375
+ console.error(err);
376
+ }
377
+ };
378
+
379
+ writeAccBtn.onclick = async () => {
380
+ if (!isConnected) { log('Error: Not connected'); return; }
381
+ const id = parseInt(servoIdInput.value, 10);
382
+ const acc = parseInt(accelerationWriteInput.value, 10);
383
+ log(`Writing acceleration ${acc} to servo ${id}...`);
384
+ try {
385
+ // Use scsServoSDK - throws on error
386
+ await scsServoSDK.writeAcceleration(id, acc);
387
+ log(`Successfully wrote acceleration ${acc} to servo ${id}.`);
388
+ } catch (err) {
389
+ log(`Error writing acceleration for servo ${id}: ${err.message}`);
390
+ console.error(err);
391
+ }
392
+ };
393
+
394
+ setWheelModeBtn.onclick = async () => {
395
+ if (!isConnected) { log('Error: Not connected'); return; }
396
+ const id = parseInt(servoIdInput.value, 10);
397
+ log(`Setting servo ${id} to wheel mode...`);
398
+ try {
399
+ // Use scsServoSDK - throws on error
400
+ await scsServoSDK.setWheelMode(id);
401
+ log(`Successfully set servo ${id} to wheel mode.`);
402
+ } catch (err) {
403
+ log(`Error setting wheel mode for servo ${id}: ${err.message}`);
404
+ console.error(err);
405
+ }
406
+ };
407
+
408
+ // Add event listener for the new button
409
+ removeWheelModeBtn.onclick = async () => {
410
+ if (!isConnected) { log('Error: Not connected'); return; }
411
+ const id = parseInt(servoIdInput.value, 10);
412
+ log(`Setting servo ${id} back to position mode...`);
413
+ try {
414
+ // Use scsServoSDK - throws on error
415
+ await scsServoSDK.setPositionMode(id);
416
+ log(`Successfully set servo ${id} back to position mode.`);
417
+ } catch (err) {
418
+ log(`Error setting position mode for servo ${id}: ${err.message}`);
419
+ console.error(err);
420
+ }
421
+ };
422
+
423
+ writeWheelSpeedBtn.onclick = async () => {
424
+ if (!isConnected) { log('Error: Not connected'); return; }
425
+ const id = parseInt(servoIdInput.value, 10);
426
+ const speed = parseInt(wheelSpeedWriteInput.value, 10);
427
+ log(`Writing wheel speed ${speed} to servo ${id}...`);
428
+ try {
429
+ // Use scsServoSDK - throws on error
430
+ await scsServoSDK.writeWheelSpeed(id, speed);
431
+ log(`Successfully wrote wheel speed ${speed} to servo ${id}.`);
432
+ } catch (err) {
433
+ log(`Error writing wheel speed for servo ${id}: ${err.message}`);
434
+ console.error(err);
435
+ }
436
+ };
437
+
438
+ syncReadBtn.onclick = async () => {
439
+ if (!isConnected) { log('Error: Not connected'); return; }
440
+ const idsString = syncReadIdsInput.value;
441
+ const ids = idsString.split(',').map(s => parseInt(s.trim(), 10)).filter(id => !isNaN(id) && id > 0 && id < 253);
442
+ if (ids.length === 0) {
443
+ log('Sync Read: No valid servo IDs provided.');
444
+ return;
445
+ }
446
+ log(`Sync reading positions for servos: ${ids.join(', ')}...`);
447
+ try {
448
+ // Use scsServoSDK - returns Map or throws
449
+ const positions = await scsServoSDK.syncReadPositions(ids);
450
+ let logMsg = 'Sync Read Successful:\n';
451
+ positions.forEach((pos, id) => {
452
+ logMsg += ` Servo ${id}: Position=${pos}\n`;
453
+ });
454
+ log(logMsg.trim());
455
+
456
+ } catch (err) {
457
+ log(`Sync Read Failed: ${err.message}`);
458
+ console.error(err);
459
+ }
460
+ };
461
+
462
+ syncWriteBtn.onclick = async () => {
463
+ if (!isConnected) { log('Error: Not connected'); return; }
464
+ const dataString = syncWriteDataInput.value;
465
+ const positionMap = new Map();
466
+ const pairs = dataString.split(',');
467
+ let validData = false;
468
+
469
+ pairs.forEach(pair => {
470
+ const parts = pair.split(':');
471
+ if (parts.length === 2) {
472
+ const id = parseInt(parts[0].trim(), 10);
473
+ const pos = parseInt(parts[1].trim(), 10);
474
+ // Position validation (0-4095)
475
+ if (!isNaN(id) && id > 0 && id < 253 && !isNaN(pos) && pos >= 0 && pos <= 4095) {
476
+ positionMap.set(id, pos);
477
+ validData = true;
478
+ } else {
479
+ log(`Sync Write Position: Invalid data pair "${pair}". ID (1-252), Pos (0-4095). Skipping.`);
480
+ }
481
+ } else {
482
+ log(`Sync Write Position: Invalid format "${pair}". Skipping.`);
483
+ }
484
+ });
485
+
486
+ if (!validData) {
487
+ log('Sync Write Position: No valid servo position data provided.');
488
+ return;
489
+ }
490
+
491
+ log(`Sync writing positions: ${Array.from(positionMap.entries()).map(([id, pos]) => `${id}:${pos}`).join(', ')}...`);
492
+ try {
493
+ // Use scsServoSDK - throws on error
494
+ await scsServoSDK.syncWritePositions(positionMap);
495
+ log(`Sync write position command sent successfully.`);
496
+ } catch (err) {
497
+ log(`Sync Write Position Failed: ${err.message}`);
498
+ console.error(err);
499
+ }
500
+ };
501
+
502
+ // New handler for Sync Write Speed
503
+ syncWriteSpeedBtn.onclick = async () => {
504
+ if (!isConnected) { log('Error: Not connected'); return; }
505
+ const dataString = syncWriteSpeedDataInput.value;
506
+ const speedMap = new Map();
507
+ const pairs = dataString.split(',');
508
+ let validData = false;
509
+
510
+ pairs.forEach(pair => {
511
+ const parts = pair.split(':');
512
+ if (parts.length === 2) {
513
+ const id = parseInt(parts[0].trim(), 10);
514
+ const speed = parseInt(parts[1].trim(), 10);
515
+ // Speed validation (-10000 to 10000)
516
+ if (!isNaN(id) && id > 0 && id < 253 && !isNaN(speed) && speed >= -10000 && speed <= 10000) {
517
+ speedMap.set(id, speed);
518
+ validData = true;
519
+ } else {
520
+ log(`Sync Write Speed: Invalid data pair "${pair}". ID (1-252), Speed (-10000 to 10000). Skipping.`);
521
+ }
522
+ } else {
523
+ log(`Sync Write Speed: Invalid format "${pair}". Skipping.`);
524
+ }
525
+ });
526
+
527
+ if (!validData) {
528
+ log('Sync Write Speed: No valid servo speed data provided.');
529
+ return;
530
+ }
531
+
532
+ log(`Sync writing speeds: ${Array.from(speedMap.entries()).map(([id, speed]) => `${id}:${speed}`).join(', ')}...`);
533
+ try {
534
+ // Use scsServoSDK - throws on error
535
+ await scsServoSDK.syncWriteWheelSpeed(speedMap);
536
+ log(`Sync write speed command sent successfully.`);
537
+ } catch (err) {
538
+ log(`Sync Write Speed Failed: ${err.message}`);
539
+ console.error(err);
540
+ }
541
+ };
542
+
543
+
544
+ scanServosBtn.onclick = async () => {
545
+ if (!isConnected) { log('Error: Not connected'); return; }
546
+
547
+ const startId = parseInt(scanStartIdInput.value, 10);
548
+ const endId = parseInt(scanEndIdInput.value, 10);
549
+
550
+ if (isNaN(startId) || isNaN(endId) || startId < 1 || endId > 252 || startId > endId) {
551
+ const errorMsg = 'Error: Invalid scan ID range. Please enter values between 1 and 252, with Start ID <= End ID.';
552
+ log(errorMsg);
553
+ scanResultsOutput.textContent = errorMsg; // Show error in results area too
554
+ return;
555
+ }
556
+
557
+ const startMsg = `Starting servo scan (IDs ${startId}-${endId})...`;
558
+ log(startMsg);
559
+ scanResultsOutput.textContent = startMsg + '\n'; // Clear and start results area
560
+ scanServosBtn.disabled = true; // Disable button during scan
561
+
562
+ let foundCount = 0;
563
+
564
+ for (let id = startId; id <= endId; id++) {
565
+ let resultMsg = `Scanning ID ${id}... `;
566
+ try {
567
+ // Attempt to read position. If it succeeds, the servo exists.
568
+ // If it throws, the servo likely doesn't exist or there's another issue.
569
+ const position = await scsServoSDK.readPosition(id);
570
+ foundCount++;
571
+
572
+ // Servo found, now try to read mode and baud rate
573
+ let mode = 'ReadError';
574
+ let baudRateIndex = 'ReadError';
575
+ try {
576
+ mode = await scsServoSDK.readMode(id);
577
+ } catch (modeErr) {
578
+ log(` Servo ${id}: Error reading mode: ${modeErr.message}`);
579
+ }
580
+ try {
581
+ baudRateIndex = await scsServoSDK.readBaudRate(id);
582
+ } catch (baudErr) {
583
+ log(` Servo ${id}: Error reading baud rate: ${baudErr.message}`);
584
+ }
585
+
586
+ resultMsg += `FOUND: Pos=${position}, Mode=${mode}, BaudIdx=${baudRateIndex}`;
587
+ log(` Servo ${id} FOUND: Position=${position}, Mode=${mode}, BaudIndex=${baudRateIndex}`);
588
+
589
+ } catch (err) {
590
+ // Check if the error message indicates a timeout or non-response, which is expected for non-existent IDs
591
+ // This check might need refinement based on the exact error messages thrown by readPosition
592
+ if (err.message.includes('timeout') || err.message.includes('No response') || err.message.includes('failed: RX')) {
593
+ resultMsg += `No response`;
594
+ // log(` Servo ${id}: No response`); // Optional: reduce log noise
595
+ } else {
596
+ // Log other unexpected errors
597
+ resultMsg += `Error: ${err.message}`;
598
+ log(` Servo ${id}: Error during scan: ${err.message}`);
599
+ console.error(`Error scanning servo ${id}:`, err);
600
+ }
601
+ }
602
+ scanResultsOutput.textContent += resultMsg + '\n'; // Append result to the results area
603
+ scanResultsOutput.scrollTop = scanResultsOutput.scrollHeight; // Auto-scroll
604
+ // Optional small delay between scans if needed
605
+ // await new Promise(resolve => setTimeout(resolve, 10));
606
+ }
607
+
608
+ const finishMsg = `Servo scan finished. Found ${foundCount} servo(s).`;
609
+ log(finishMsg);
610
+ scanResultsOutput.textContent += finishMsg + '\n'; // Add finish message to results area
611
+ scanResultsOutput.scrollTop = scanResultsOutput.scrollHeight; // Auto-scroll
612
+ scanServosBtn.disabled = false; // Re-enable button
613
+ };
614
+
615
+ // Initial log
616
+ log('Test page loaded. Please connect to a servo controller.');
617
+
618
+ </script>
619
+ </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
620
  </html>
src/lib/components/3d/elements/robot/modal/InputConnectionModal.svelte CHANGED
@@ -41,13 +41,6 @@
41
  }
42
  });
43
 
44
- // Set up calibration completion callback
45
- $effect(() => {
46
- robot.calibrationManager.onCalibrationCompleteWithPositions((finalPositions) => {
47
- robot.syncToCalibrationPositions(finalPositions);
48
- });
49
- });
50
-
51
  async function refreshRooms() {
52
  try {
53
  error = null;
@@ -129,17 +122,20 @@
129
  isConnecting = true;
130
  error = null;
131
 
132
- if (robot.calibrationManager.needsCalibration) {
133
- pendingUSBConnection = "input";
134
- showUSBCalibration = true;
135
- return;
136
- }
137
-
138
  await robot.setConsumer({
139
  type: "usb",
140
  baudRate: 1000000
141
  });
142
 
 
 
 
 
 
 
 
 
143
  toast.success("USB Input Connected", {
144
  description: "Successfully connected to physical robot hardware"
145
  });
@@ -174,18 +170,22 @@
174
 
175
  async function onCalibrationComplete() {
176
  showUSBCalibration = false;
177
-
178
- if (pendingUSBConnection === "input") {
179
- await connectUSBInput();
180
- }
181
-
182
  pendingUSBConnection = null;
 
 
 
 
183
  }
184
 
185
  function onCalibrationCancel() {
186
  showUSBCalibration = false;
187
  pendingUSBConnection = null;
188
  isConnecting = false;
 
 
 
 
 
189
  }
190
  </script>
191
 
@@ -252,12 +252,19 @@
252
  </Alert.Description>
253
  </Alert.Root>
254
 
255
- <USBCalibrationPanel
256
- calibrationManager={robot.calibrationManager}
257
- connectionType="consumer"
258
- {onCalibrationComplete}
259
- onCancel={onCalibrationCancel}
260
- />
 
 
 
 
 
 
 
261
  </Card.Content>
262
  </Card.Root>
263
  {:else}
 
41
  }
42
  });
43
 
 
 
 
 
 
 
 
44
  async function refreshRooms() {
45
  try {
46
  error = null;
 
122
  isConnecting = true;
123
  error = null;
124
 
125
+ // Create USB consumer first, which will handle calibration internally
 
 
 
 
 
126
  await robot.setConsumer({
127
  type: "usb",
128
  baudRate: 1000000
129
  });
130
 
131
+ // Check if the new consumer needs calibration
132
+ const uncalibratedDrivers = robot.getUncalibratedUSBDrivers();
133
+ if (uncalibratedDrivers.length > 0) {
134
+ pendingUSBConnection = "input";
135
+ showUSBCalibration = true;
136
+ return;
137
+ }
138
+
139
  toast.success("USB Input Connected", {
140
  description: "Successfully connected to physical robot hardware"
141
  });
 
170
 
171
  async function onCalibrationComplete() {
172
  showUSBCalibration = false;
 
 
 
 
 
173
  pendingUSBConnection = null;
174
+
175
+ toast.success("Calibration Complete", {
176
+ description: "Hardware calibrated and ready for use"
177
+ });
178
  }
179
 
180
  function onCalibrationCancel() {
181
  showUSBCalibration = false;
182
  pendingUSBConnection = null;
183
  isConnecting = false;
184
+
185
+ // Clean up the uncalibrated USB consumer
186
+ robot.removeConsumer().catch(err => {
187
+ console.error("Failed to clean up USB consumer after calibration cancel:", err);
188
+ });
189
  }
190
  </script>
191
 
 
252
  </Alert.Description>
253
  </Alert.Root>
254
 
255
+ <!-- Show calibration panels for each uncalibrated USB driver -->
256
+ {#each robot.getUncalibratedUSBDrivers() as usbDriver}
257
+ <USBCalibrationPanel
258
+ calibrationManager={usbDriver}
259
+ connectionType="consumer"
260
+ {onCalibrationComplete}
261
+ onCancel={onCalibrationCancel}
262
+ />
263
+ {:else}
264
+ <div class="text-center text-slate-400">
265
+ No USB drivers require calibration
266
+ </div>
267
+ {/each}
268
  </Card.Content>
269
  </Card.Root>
270
  {:else}
src/lib/components/3d/elements/robot/modal/OutputConnectionModal.svelte CHANGED
@@ -50,13 +50,6 @@
50
  }
51
  });
52
 
53
- // Set up calibration completion callback
54
- $effect(() => {
55
- robot.calibrationManager.onCalibrationCompleteWithPositions((finalPositions) => {
56
- robot.syncToCalibrationPositions(finalPositions);
57
- });
58
- });
59
-
60
  // Room management functions
61
  async function refreshRooms() {
62
  try {
@@ -138,18 +131,20 @@
138
  isConnecting = true;
139
  error = null;
140
 
141
- // Check if calibration is needed
142
- if (robot.calibrationManager.needsCalibration) {
143
- pendingUSBConnection = "output";
144
- showUSBCalibration = true;
145
- return;
146
- }
147
-
148
  await robot.addProducer({
149
  type: "usb",
150
  baudRate: 1000000
151
  });
152
 
 
 
 
 
 
 
 
 
153
  toast.success("USB Output Connected", {
154
  description: "Successfully connected to physical robot hardware"
155
  });
@@ -185,18 +180,29 @@
185
  // Handle calibration completion
186
  async function onCalibrationComplete() {
187
  showUSBCalibration = false;
188
-
189
- if (pendingUSBConnection === "output") {
190
- await connectUSBOutput();
191
- }
192
-
193
  pendingUSBConnection = null;
 
 
 
 
194
  }
195
 
196
  function onCalibrationCancel() {
197
  showUSBCalibration = false;
198
  pendingUSBConnection = null;
199
  isConnecting = false;
 
 
 
 
 
 
 
 
 
 
 
 
200
  }
201
  </script>
202
 
@@ -262,12 +268,19 @@
262
  </Alert.Description>
263
  </Alert.Root>
264
 
265
- <USBCalibrationPanel
266
- calibrationManager={robot.calibrationManager}
267
- connectionType="producer"
268
- {onCalibrationComplete}
269
- onCancel={onCalibrationCancel}
270
- />
 
 
 
 
 
 
 
271
  </Card.Content>
272
  </Card.Root>
273
  {:else}
 
50
  }
51
  });
52
 
 
 
 
 
 
 
 
53
  // Room management functions
54
  async function refreshRooms() {
55
  try {
 
131
  isConnecting = true;
132
  error = null;
133
 
134
+ // Create USB producer first, which will handle calibration internally
 
 
 
 
 
 
135
  await robot.addProducer({
136
  type: "usb",
137
  baudRate: 1000000
138
  });
139
 
140
+ // Check if any USB drivers need calibration
141
+ const uncalibratedDrivers = robot.getUncalibratedUSBDrivers();
142
+ if (uncalibratedDrivers.length > 0) {
143
+ pendingUSBConnection = "output";
144
+ showUSBCalibration = true;
145
+ return;
146
+ }
147
+
148
  toast.success("USB Output Connected", {
149
  description: "Successfully connected to physical robot hardware"
150
  });
 
180
  // Handle calibration completion
181
  async function onCalibrationComplete() {
182
  showUSBCalibration = false;
 
 
 
 
 
183
  pendingUSBConnection = null;
184
+
185
+ toast.success("Calibration Complete", {
186
+ description: "Hardware calibrated and ready for use"
187
+ });
188
  }
189
 
190
  function onCalibrationCancel() {
191
  showUSBCalibration = false;
192
  pendingUSBConnection = null;
193
  isConnecting = false;
194
+
195
+ // Clean up the uncalibrated USB producer
196
+ const uncalibratedDrivers = robot.getUncalibratedUSBDrivers();
197
+ if (uncalibratedDrivers.length > 0) {
198
+ // Remove the most recent producer (should be the one we just added)
199
+ const lastProducer = robot.producers[robot.producers.length - 1];
200
+ if (lastProducer) {
201
+ robot.removeProducer(lastProducer.id).catch(err => {
202
+ console.error("Failed to clean up USB producer after calibration cancel:", err);
203
+ });
204
+ }
205
+ }
206
  }
207
  </script>
208
 
 
268
  </Alert.Description>
269
  </Alert.Root>
270
 
271
+ <!-- Show calibration panels for each uncalibrated USB driver -->
272
+ {#each robot.getUncalibratedUSBDrivers() as usbDriver}
273
+ <USBCalibrationPanel
274
+ calibrationManager={usbDriver}
275
+ connectionType="producer"
276
+ {onCalibrationComplete}
277
+ onCancel={onCalibrationCancel}
278
+ />
279
+ {:else}
280
+ <div class="text-center text-slate-400">
281
+ No USB drivers require calibration
282
+ </div>
283
+ {/each}
284
  </Card.Content>
285
  </Card.Root>
286
  {:else}
src/lib/elements/robot/Robot.svelte.ts CHANGED
@@ -12,8 +12,8 @@ import { USBConsumer } from './drivers/USBConsumer.js';
12
  import { USBProducer } from './drivers/USBProducer.js';
13
  import { RemoteConsumer } from './drivers/RemoteConsumer.js';
14
  import { RemoteProducer } from './drivers/RemoteProducer.js';
15
- import { USBCalibrationManager } from './calibration/USBCalibrationManager.js';
16
- import type { UrdfRobotState } from '@/types/robot.js';
17
  import { ROBOT_CONFIG } from './config.js';
18
  import type IUrdfRobot from '@/components/3d/elements/robot/URDF/interfaces/IUrdfRobot.js';
19
 
@@ -46,9 +46,6 @@ export class Robot implements Positionable {
46
  // URDF robot state for 3D visualization - PUBLIC for reactive access
47
  urdfRobotState = $state<IUrdfRobot | null>(null);
48
 
49
- // Shared USB calibration manager for this robot
50
- private usbCalibrationManager: USBCalibrationManager = new USBCalibrationManager();
51
-
52
  // Derived reactive values for components
53
  jointArray = $derived(Object.values(this.joints));
54
  hasProducers = $derived(this.producers.length > 0);
@@ -83,49 +80,53 @@ export class Robot implements Positionable {
83
  this.position = { ...newPosition };
84
  }
85
 
86
- // Calibration access
87
- get calibrationManager(): USBCalibrationManager {
88
- return this.usbCalibrationManager;
89
- }
90
-
91
- // NEW: Sync virtual robot to final calibration positions
92
- syncToCalibrationPositions(finalPositions: Record<string, number>): void {
93
- console.log(`[Robot ${this.id}] 🔄 Syncing virtual robot to final calibration positions...`);
94
 
95
- Object.entries(finalPositions).forEach(([jointName, rawPosition]) => {
96
- const joint = this.joints[jointName];
97
- if (!joint) {
98
- console.warn(`[Robot ${this.id}] Joint ${jointName} not found for position sync`);
99
- return;
100
- }
101
-
102
- // Convert raw servo position to normalized value using calibration data
103
- const normalizedValue = this.usbCalibrationManager.normalizeValue(rawPosition, jointName);
104
-
105
- // Clamp to appropriate normalized range based on joint type
106
- let clampedValue: number;
107
- if (jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper') {
108
- clampedValue = Math.max(0, Math.min(100, normalizedValue));
109
- } else {
110
- clampedValue = Math.max(-100, Math.min(100, normalizedValue));
111
  }
112
-
113
- console.log(`[Robot ${this.id}] ${jointName}: ${rawPosition} (raw) -> ${normalizedValue.toFixed(1)} -> ${clampedValue.toFixed(1)} (normalized)`);
114
-
115
- // Update joint value to match physical servo position
116
- this.joints[jointName] = { ...joint, value: clampedValue };
117
  });
118
 
119
- console.log(`[Robot ${this.id}] ✅ Virtual robot synced to calibration positions`);
120
  }
121
 
122
- // Joint value updates (normalized)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  updateJoint(name: string, normalizedValue: number): void {
124
  if (!this.isManualControlEnabled) {
125
  console.warn('Manual control is disabled');
126
  return;
127
  }
128
 
 
 
 
 
 
129
  const joint = this.joints[name];
130
  if (!joint) {
131
  console.warn(`Joint ${name} not found`);
@@ -139,13 +140,15 @@ export class Robot implements Positionable {
139
  normalizedValue = Math.max(-100, Math.min(100, normalizedValue));
140
  }
141
 
142
- console.debug(`[Robot ${this.id}] Manual update joint ${name} to ${normalizedValue} (normalized)`);
143
 
144
  // Create a new joint object to ensure reactivity
145
  this.joints[name] = { ...joint, value: normalizedValue };
146
 
147
- // Send normalized command to producers
148
- this.sendToProducers({ joints: [{ name, value: normalizedValue }] });
 
 
149
  }
150
 
151
  executeCommand(command: RobotCommand): void {
@@ -183,27 +186,7 @@ export class Robot implements Positionable {
183
  console.debug(`[Robot ${this.id}] Executing command with ${command.joints.length} joints:`,
184
  command.joints.map(j => `${j.name}=${j.value}`).join(', '));
185
 
186
- // Check if USB calibration is in progress
187
- if (this.usbCalibrationManager.calibrationState.isCalibrating) {
188
- console.debug(`[Robot ${this.id}] 🚫 Blocking virtual robot updates - USB calibration in progress`);
189
- // Still send to producers, but don't update virtual robot
190
- this.sendToProducers(command);
191
- return;
192
- }
193
-
194
- // Check if USB calibration is needed (if we have USB consumer/producers)
195
- const hasUSBDrivers = (this.consumer instanceof USBConsumer) ||
196
- this.producers.some(p => p instanceof USBProducer);
197
-
198
- if (hasUSBDrivers && this.usbCalibrationManager.needsCalibration) {
199
- console.debug(`[Robot ${this.id}] ⏳ Blocking virtual robot updates - USB drivers need calibration`);
200
- // Still send to producers, but don't update virtual robot
201
- this.sendToProducers(command);
202
- return;
203
- }
204
-
205
- console.debug(`[Robot ${this.id}] ✅ Updating virtual robot - USB calibrated or no USB drivers`);
206
-
207
  command.joints.forEach(jointCmd => {
208
  const joint = this.joints[jointCmd.name];
209
  if (joint) {
@@ -273,9 +256,30 @@ export class Robot implements Positionable {
273
 
274
  const consumer = this.createConsumer(config);
275
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  // Only pass joinExistingRoom to remote drivers
277
  if (config.type === 'remote') {
278
- await (consumer as any).connect(joinExistingRoom);
279
  } else {
280
  await consumer.connect();
281
  }
@@ -292,9 +296,18 @@ export class Robot implements Positionable {
292
  });
293
  this.unsubscribeFns.push(statusUnsubscribe);
294
 
295
- // Start listening for consumers with this capability
296
  if ('startListening' in consumer && consumer.startListening) {
297
- await consumer.startListening();
 
 
 
 
 
 
 
 
 
298
  }
299
 
300
  this.consumer = consumer;
@@ -319,9 +332,22 @@ export class Robot implements Positionable {
319
  private async _addProducer(config: USBDriverConfig | RemoteDriverConfig, joinExistingRoom: boolean): Promise<string> {
320
  const producer = this.createProducer(config);
321
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  // Only pass joinExistingRoom to remote drivers
323
  if (config.type === 'remote') {
324
- await (producer as any).connect(joinExistingRoom);
325
  } else {
326
  await producer.connect();
327
  }
@@ -366,7 +392,7 @@ export class Robot implements Positionable {
366
  private createConsumer(config: USBDriverConfig | RemoteDriverConfig): Consumer {
367
  switch (config.type) {
368
  case 'usb':
369
- return new USBConsumer(config, this.usbCalibrationManager);
370
  case 'remote':
371
  return new RemoteConsumer(config);
372
  default:
@@ -378,7 +404,7 @@ export class Robot implements Positionable {
378
  private createProducer(config: USBDriverConfig | RemoteDriverConfig): Producer {
379
  switch (config.type) {
380
  case 'usb':
381
- return new USBProducer(config, this.usbCalibrationManager);
382
  case 'remote':
383
  return new RemoteProducer(config);
384
  default:
@@ -465,7 +491,6 @@ export class Robot implements Positionable {
465
  })
466
  );
467
 
468
- // Clean up calibration manager
469
- await this.usbCalibrationManager.destroy();
470
  }
471
  }
 
12
  import { USBProducer } from './drivers/USBProducer.js';
13
  import { RemoteConsumer } from './drivers/RemoteConsumer.js';
14
  import { RemoteProducer } from './drivers/RemoteProducer.js';
15
+ import { USBServoDriver } from './drivers/USBServoDriver.js';
16
+
17
  import { ROBOT_CONFIG } from './config.js';
18
  import type IUrdfRobot from '@/components/3d/elements/robot/URDF/interfaces/IUrdfRobot.js';
19
 
 
46
  // URDF robot state for 3D visualization - PUBLIC for reactive access
47
  urdfRobotState = $state<IUrdfRobot | null>(null);
48
 
 
 
 
49
  // Derived reactive values for components
50
  jointArray = $derived(Object.values(this.joints));
51
  hasProducers = $derived(this.producers.length > 0);
 
80
  this.position = { ...newPosition };
81
  }
82
 
83
+ // Get all USB drivers (both consumer and producers) for calibration
84
+ getUSBDrivers(): USBServoDriver[] {
85
+ const usbDrivers: USBServoDriver[] = [];
 
 
 
 
 
86
 
87
+ // Check consumer
88
+ if (this.consumer && USBServoDriver.isUSBDriver(this.consumer)) {
89
+ usbDrivers.push(this.consumer);
90
+ }
91
+
92
+ // Check producers
93
+ this.producers.forEach(producer => {
94
+ if (USBServoDriver.isUSBDriver(producer)) {
95
+ usbDrivers.push(producer);
 
 
 
 
 
 
 
96
  }
 
 
 
 
 
97
  });
98
 
99
+ return usbDrivers;
100
  }
101
 
102
+ // Get uncalibrated USB drivers that need calibration
103
+ getUncalibratedUSBDrivers(): USBServoDriver[] {
104
+ return this.getUSBDrivers().filter(driver => driver.needsCalibration);
105
+ }
106
+
107
+ // Check if robot has any USB drivers
108
+ hasUSBDrivers(): boolean {
109
+ return this.getUSBDrivers().length > 0;
110
+ }
111
+
112
+ // Check if all USB drivers are calibrated
113
+ areAllUSBDriversCalibrated(): boolean {
114
+ const usbDrivers = this.getUSBDrivers();
115
+ return usbDrivers.length > 0 && usbDrivers.every(driver => driver.isCalibrated);
116
+ }
117
+
118
+ // Joint value updates (normalized) - for manual control
119
  updateJoint(name: string, normalizedValue: number): void {
120
  if (!this.isManualControlEnabled) {
121
  console.warn('Manual control is disabled');
122
  return;
123
  }
124
 
125
+ this.updateJointValue(name, normalizedValue, true);
126
+ }
127
+
128
+ // Internal joint value update (used by both manual control and USB calibration sync)
129
+ updateJointValue(name: string, normalizedValue: number, sendToProducers: boolean = false): void {
130
  const joint = this.joints[name];
131
  if (!joint) {
132
  console.warn(`Joint ${name} not found`);
 
140
  normalizedValue = Math.max(-100, Math.min(100, normalizedValue));
141
  }
142
 
143
+ console.debug(`[Robot ${this.id}] Update joint ${name} to ${normalizedValue} (normalized, sendToProducers: ${sendToProducers})`);
144
 
145
  // Create a new joint object to ensure reactivity
146
  this.joints[name] = { ...joint, value: normalizedValue };
147
 
148
+ // Send normalized command to producers if requested
149
+ if (sendToProducers) {
150
+ this.sendToProducers({ joints: [{ name, value: normalizedValue }] });
151
+ }
152
  }
153
 
154
  executeCommand(command: RobotCommand): void {
 
186
  console.debug(`[Robot ${this.id}] Executing command with ${command.joints.length} joints:`,
187
  command.joints.map(j => `${j.name}=${j.value}`).join(', '));
188
 
189
+ // Update virtual robot joints with normalized values
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  command.joints.forEach(jointCmd => {
191
  const joint = this.joints[jointCmd.name];
192
  if (joint) {
 
256
 
257
  const consumer = this.createConsumer(config);
258
 
259
+ // Set up calibration completion callback for USB drivers
260
+ if (USBServoDriver.isUSBDriver(consumer)) {
261
+ const calibrationUnsubscribe = consumer.onCalibrationCompleteWithPositions(async (finalPositions: Record<string, number>) => {
262
+ console.log(`[Robot ${this.id}] Calibration complete, syncing robot to final positions`);
263
+ consumer.syncRobotPositions(finalPositions, (jointName: string, normalizedValue: number) => {
264
+ this.updateJointValue(jointName, normalizedValue, false); // Don't send to producers to avoid feedback loop
265
+ });
266
+
267
+ // Start listening now that calibration is complete
268
+ if ('startListening' in consumer && consumer.startListening) {
269
+ try {
270
+ await consumer.startListening();
271
+ console.log(`[Robot ${this.id}] Started listening after calibration completion`);
272
+ } catch (error) {
273
+ console.error(`[Robot ${this.id}] Failed to start listening after calibration:`, error);
274
+ }
275
+ }
276
+ });
277
+ this.unsubscribeFns.push(calibrationUnsubscribe);
278
+ }
279
+
280
  // Only pass joinExistingRoom to remote drivers
281
  if (config.type === 'remote') {
282
+ await (consumer as RemoteConsumer).connect(joinExistingRoom);
283
  } else {
284
  await consumer.connect();
285
  }
 
296
  });
297
  this.unsubscribeFns.push(statusUnsubscribe);
298
 
299
+ // Start listening for consumers with this capability (only if calibrated for USB)
300
  if ('startListening' in consumer && consumer.startListening) {
301
+ // For USB consumers, only start listening if calibrated
302
+ if (USBServoDriver.isUSBDriver(consumer)) {
303
+ if (consumer.isCalibrated) {
304
+ await consumer.startListening();
305
+ }
306
+ // If not calibrated, startListening will be called after calibration completion
307
+ } else {
308
+ // For non-USB consumers, start listening immediately
309
+ await consumer.startListening();
310
+ }
311
  }
312
 
313
  this.consumer = consumer;
 
332
  private async _addProducer(config: USBDriverConfig | RemoteDriverConfig, joinExistingRoom: boolean): Promise<string> {
333
  const producer = this.createProducer(config);
334
 
335
+ // Set up calibration completion callback for USB drivers
336
+ if (USBServoDriver.isUSBDriver(producer)) {
337
+ const calibrationUnsubscribe = producer.onCalibrationCompleteWithPositions(async (finalPositions: Record<string, number>) => {
338
+ console.log(`[Robot ${this.id}] Calibration complete, syncing robot to final positions`);
339
+ producer.syncRobotPositions(finalPositions, (jointName: string, normalizedValue: number) => {
340
+ this.updateJointValue(jointName, normalizedValue, false); // Don't send to producers to avoid feedback loop
341
+ });
342
+
343
+ console.log(`[Robot ${this.id}] USB Producer calibration completed and ready for commands`);
344
+ });
345
+ this.unsubscribeFns.push(calibrationUnsubscribe);
346
+ }
347
+
348
  // Only pass joinExistingRoom to remote drivers
349
  if (config.type === 'remote') {
350
+ await (producer as RemoteProducer).connect(joinExistingRoom);
351
  } else {
352
  await producer.connect();
353
  }
 
392
  private createConsumer(config: USBDriverConfig | RemoteDriverConfig): Consumer {
393
  switch (config.type) {
394
  case 'usb':
395
+ return new USBConsumer(config);
396
  case 'remote':
397
  return new RemoteConsumer(config);
398
  default:
 
404
  private createProducer(config: USBDriverConfig | RemoteDriverConfig): Producer {
405
  switch (config.type) {
406
  case 'usb':
407
+ return new USBProducer(config);
408
  case 'remote':
409
  return new RemoteProducer(config);
410
  default:
 
491
  })
492
  );
493
 
494
+ // Calibration cleanup is handled by individual USB drivers
 
495
  }
496
  }
src/lib/elements/robot/calibration/CalibrationState.svelte.ts CHANGED
@@ -1,114 +1,271 @@
1
- import type { USBCalibrationManager } from './USBCalibrationManager.js';
2
  import type { JointCalibration } from '../models.js';
3
  import { ROBOT_CONFIG } from '../config.js';
4
 
5
  export class CalibrationState {
6
- private manager: USBCalibrationManager;
 
 
7
 
8
- // Reactive state using Svelte 5 runes - direct state management
9
- private _isCalibrating = $state(false);
10
- private _progress = $state(0);
11
- private _isCalibrated = $state(false);
12
- private _needsCalibration = $state(true);
13
- private _jointValues = $state<Record<string, number>>({});
14
- private _jointCalibrations = $state<Record<string, JointCalibration>>({});
15
 
16
- constructor(manager: USBCalibrationManager) {
17
- this.manager = manager;
18
-
19
- // Initialize reactive state
20
- manager.jointNames_.forEach(name => {
21
- this._jointValues[name] = 0;
22
- this._jointCalibrations[name] = { isCalibrated: false };
 
 
 
 
 
 
23
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
- // Subscribe to manager changes
26
- this.setupManagerSubscription();
27
-
28
- // Initial state sync
29
- this.syncManagerState();
 
 
 
 
 
 
 
 
 
 
 
30
  }
31
-
32
- // Reactive getters - now use internal reactive state
33
- get isCalibrating(): boolean { return this._isCalibrating; }
34
- get progress(): number { return this._progress; }
35
- get isCalibrated(): boolean { return this._isCalibrated; }
36
- get needsCalibration(): boolean { return this._needsCalibration; }
37
- get jointValues(): Record<string, number> { return this._jointValues; }
38
- get jointCalibrations(): Record<string, JointCalibration> { return this._jointCalibrations; }
39
-
40
- // Get current value for a specific joint
41
  getCurrentValue(jointName: string): number | undefined {
42
- return this._jointValues[jointName];
43
  }
44
-
45
- // Get calibration for a specific joint
46
  getJointCalibration(jointName: string): JointCalibration | undefined {
47
- return this._jointCalibrations[jointName];
48
  }
49
-
50
- // Get range for a specific joint
51
- getJointRange(jointName: string): number {
52
- const calibration = this._jointCalibrations[jointName];
53
- if (!calibration?.minServoValue || !calibration?.maxServoValue) return 0;
54
- return Math.abs(calibration.maxServoValue - calibration.minServoValue);
 
 
55
  }
56
-
57
- private updateInterval: Timer | null = null;
58
- private managerUnsubscribe: (() => void) | null = null;
59
-
60
- private setupManagerSubscription(): void {
61
- // Use centralized config for UI update frequency
62
- this.updateInterval = setInterval(() => {
63
- this.syncManagerState();
64
- }, ROBOT_CONFIG.polling.uiUpdateRate); // Centralized UI update rate
 
 
65
 
66
- // Also listen to manager calibration changes for immediate updates
67
- const unsubscribe = this.manager.onCalibrationChange(() => {
68
- console.debug('[CalibrationState] Manager calibration changed, syncing state');
69
- this.syncManagerState();
 
 
 
70
  });
71
-
72
- // Store unsubscribe function for cleanup
73
- this.managerUnsubscribe = unsubscribe;
74
  }
75
 
76
- private syncManagerState(): void {
77
- // Sync manager state to reactive state
78
- this._isCalibrating = this.manager.calibrationState.isCalibrating;
79
- this._progress = this.manager.calibrationState.progress;
80
- this._isCalibrated = this.manager.isCalibrated;
81
- this._needsCalibration = this.manager.needsCalibration;
82
 
83
- // Update joint values and calibrations
84
- this.manager.jointNames_.forEach(name => {
85
- const currentValue = this.manager.getCurrentRawValue(name);
86
- if (currentValue !== undefined) {
87
- this._jointValues[name] = currentValue;
 
 
 
 
 
 
 
88
  }
89
-
90
- const calibration = this.manager.getJointCalibration(name);
91
- if (calibration) {
92
- // Create new object to ensure reactivity
93
- this._jointCalibrations[name] = { ...calibration };
 
 
 
 
 
 
94
  }
95
- });
 
 
96
  }
97
 
98
- // Cleanup method
99
- destroy(): void {
100
- if (this.updateInterval) {
101
- clearInterval(this.updateInterval);
102
- this.updateInterval = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  }
104
- if (this.managerUnsubscribe) {
105
- this.managerUnsubscribe();
106
- this.managerUnsubscribe = null;
 
 
 
 
 
 
 
 
 
 
 
107
  }
108
  }
109
-
110
- // Format servo value for display
111
- formatServoValue(value: number | undefined): string {
112
- return value !== undefined ? value.toString() : '---';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  }
114
  }
 
 
1
  import type { JointCalibration } from '../models.js';
2
  import { ROBOT_CONFIG } from '../config.js';
3
 
4
  export class CalibrationState {
5
+ // Reactive calibration state
6
+ isCalibrating = $state(false);
7
+ progress = $state(0);
8
 
9
+ // Joint calibration data
10
+ private jointCalibrations = $state<Record<string, JointCalibration>>({});
11
+ private currentValues = $state<Record<string, number>>({});
 
 
 
 
12
 
13
+ // Callbacks for completion with final positions
14
+ private completionCallbacks: Array<(positions: Record<string, number>) => void> = [];
15
+
16
+ constructor() {
17
+ // Initialize calibration data for expected joints
18
+ const jointNames = ["Rotation", "Pitch", "Elbow", "Wrist_Pitch", "Wrist_Roll", "Jaw"];
19
+ jointNames.forEach(name => {
20
+ this.jointCalibrations[name] = {
21
+ isCalibrated: false,
22
+ minServoValue: undefined,
23
+ maxServoValue: undefined
24
+ };
25
+ this.currentValues[name] = 0;
26
  });
27
+ }
28
+
29
+ // Computed properties
30
+ get needsCalibration(): boolean {
31
+ return Object.values(this.jointCalibrations).some(cal => !cal.isCalibrated);
32
+ }
33
+
34
+ get isCalibrated(): boolean {
35
+ return Object.values(this.jointCalibrations).every(cal => cal.isCalibrated);
36
+ }
37
+
38
+ // Update current servo value during calibration
39
+ updateCurrentValue(jointName: string, servoValue: number): void {
40
+ this.currentValues[jointName] = servoValue;
41
 
42
+ // Update calibration range if calibrating
43
+ if (this.isCalibrating) {
44
+ const calibration = this.jointCalibrations[jointName];
45
+ if (calibration) {
46
+ // Update min/max values
47
+ if (calibration.minServoValue === undefined || servoValue < calibration.minServoValue) {
48
+ calibration.minServoValue = servoValue;
49
+ }
50
+ if (calibration.maxServoValue === undefined || servoValue > calibration.maxServoValue) {
51
+ calibration.maxServoValue = servoValue;
52
+ }
53
+
54
+ // Update progress based on range coverage
55
+ this.updateProgress();
56
+ }
57
+ }
58
  }
59
+
60
+ // Get current value for a joint
 
 
 
 
 
 
 
 
61
  getCurrentValue(jointName: string): number | undefined {
62
+ return this.currentValues[jointName];
63
  }
64
+
65
+ // Get calibration data for a joint
66
  getJointCalibration(jointName: string): JointCalibration | undefined {
67
+ return this.jointCalibrations[jointName];
68
  }
69
+
70
+ // Get formatted range string for display
71
+ getJointRange(jointName: string): string {
72
+ const calibration = this.jointCalibrations[jointName];
73
+ if (!calibration || calibration.minServoValue === undefined || calibration.maxServoValue === undefined) {
74
+ return "Not set";
75
+ }
76
+ return `${calibration.minServoValue}-${calibration.maxServoValue}`;
77
  }
78
+
79
+ // Format servo value for display
80
+ formatServoValue(value: number | undefined): string {
81
+ return value !== undefined ? value.toString() : "---";
82
+ }
83
+
84
+ // Start calibration process
85
+ startCalibration(): void {
86
+ console.log("[CalibrationState] Starting calibration...");
87
+ this.isCalibrating = true;
88
+ this.progress = 0;
89
 
90
+ // Reset calibration data
91
+ Object.keys(this.jointCalibrations).forEach(jointName => {
92
+ this.jointCalibrations[jointName] = {
93
+ isCalibrated: false,
94
+ minServoValue: undefined,
95
+ maxServoValue: undefined
96
+ };
97
  });
 
 
 
98
  }
99
 
100
+ // Complete calibration and mark joints as calibrated
101
+ completeCalibration(): Record<string, number> {
102
+ console.log("[CalibrationState] Completing calibration...");
103
+
104
+ const finalPositions: Record<string, number> = {};
 
105
 
106
+ // Mark all joints with sufficient range as calibrated
107
+ Object.keys(this.jointCalibrations).forEach(jointName => {
108
+ const calibration = this.jointCalibrations[jointName];
109
+ if (calibration.minServoValue !== undefined && calibration.maxServoValue !== undefined) {
110
+ const range = calibration.maxServoValue - calibration.minServoValue;
111
+ if (range >= ROBOT_CONFIG.calibration.minRangeThreshold) {
112
+ calibration.isCalibrated = true;
113
+ finalPositions[jointName] = this.currentValues[jointName] || 0;
114
+ console.log(`[CalibrationState] Joint ${jointName} calibrated: ${this.getJointRange(jointName)} (range: ${range})`);
115
+ } else {
116
+ console.warn(`[CalibrationState] Joint ${jointName} range too small: ${range} < ${ROBOT_CONFIG.calibration.minRangeThreshold}`);
117
+ }
118
  }
119
+ });
120
+
121
+ this.isCalibrating = false;
122
+ this.progress = 100;
123
+
124
+ // Notify completion callbacks
125
+ this.completionCallbacks.forEach(callback => {
126
+ try {
127
+ callback(finalPositions);
128
+ } catch (error) {
129
+ console.error("[CalibrationState] Error in completion callback:", error);
130
  }
131
+ });
132
+
133
+ return finalPositions;
134
  }
135
 
136
+ // Cancel calibration
137
+ cancelCalibration(): void {
138
+ console.log("[CalibrationState] Calibration cancelled");
139
+ this.isCalibrating = false;
140
+ this.progress = 0;
141
+
142
+ // Reset calibration data
143
+ Object.keys(this.jointCalibrations).forEach(jointName => {
144
+ this.jointCalibrations[jointName] = {
145
+ isCalibrated: false,
146
+ minServoValue: undefined,
147
+ maxServoValue: undefined
148
+ };
149
+ });
150
+ }
151
+
152
+ // Skip calibration (use predefined values)
153
+ skipCalibration(): void {
154
+ console.log("[CalibrationState] Skipping calibration with predefined values");
155
+
156
+ // Set predefined calibration values for SO-100 arm
157
+ const predefinedCalibrations = {
158
+ "Rotation": { minServoValue: 500, maxServoValue: 3500, isCalibrated: true },
159
+ "Pitch": { minServoValue: 500, maxServoValue: 3500, isCalibrated: true },
160
+ "Elbow": { minServoValue: 500, maxServoValue: 3500, isCalibrated: true },
161
+ "Wrist_Pitch": { minServoValue: 500, maxServoValue: 3500, isCalibrated: true },
162
+ "Wrist_Roll": { minServoValue: 500, maxServoValue: 3500, isCalibrated: true },
163
+ "Jaw": { minServoValue: 1000, maxServoValue: 3000, isCalibrated: true }
164
+ };
165
+
166
+ Object.entries(predefinedCalibrations).forEach(([jointName, calibration]) => {
167
+ this.jointCalibrations[jointName] = calibration;
168
+ });
169
+
170
+ this.isCalibrating = false;
171
+ this.progress = 100;
172
+ }
173
+
174
+ // Convert raw servo value to normalized percentage (for USB INPUT - reading from servo)
175
+ normalizeValue(rawValue: number, jointName: string): number {
176
+ const calibration = this.jointCalibrations[jointName];
177
+ if (!calibration || !calibration.isCalibrated ||
178
+ calibration.minServoValue === undefined || calibration.maxServoValue === undefined) {
179
+ // No calibration, use appropriate default conversion
180
+ const isGripper = jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper';
181
+ if (isGripper) {
182
+ return Math.max(0, Math.min(100, (rawValue / 4095) * 100));
183
+ } else {
184
+ return Math.max(-100, Math.min(100, ((rawValue - 2048) / 2048) * 100));
185
+ }
186
  }
187
+
188
+ const { minServoValue, maxServoValue } = calibration;
189
+ if (maxServoValue === minServoValue) return 0;
190
+
191
+ // Bound the input servo value to calibrated range
192
+ const bounded = Math.max(minServoValue, Math.min(maxServoValue, rawValue));
193
+
194
+ const isGripper = jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper';
195
+ if (isGripper) {
196
+ // Gripper: 0-100%
197
+ return ((bounded - minServoValue) / (maxServoValue - minServoValue)) * 100;
198
+ } else {
199
+ // Regular joint: -100 to +100%
200
+ return (((bounded - minServoValue) / (maxServoValue - minServoValue)) * 200) - 100;
201
  }
202
  }
203
+
204
+ // Convert normalized percentage to raw servo value (for USB OUTPUT - writing to servo)
205
+ denormalizeValue(normalizedValue: number, jointName: string): number {
206
+ const calibration = this.jointCalibrations[jointName];
207
+ if (!calibration || !calibration.isCalibrated ||
208
+ calibration.minServoValue === undefined || calibration.maxServoValue === undefined) {
209
+ // No calibration, use appropriate default conversion
210
+ const isGripper = jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper';
211
+ if (isGripper) {
212
+ return Math.round((normalizedValue / 100) * 4095);
213
+ } else {
214
+ return Math.round(2048 + (normalizedValue / 100) * 2048);
215
+ }
216
+ }
217
+
218
+ const { minServoValue, maxServoValue } = calibration;
219
+ const range = maxServoValue - minServoValue;
220
+
221
+ let normalizedRatio: number;
222
+ const isGripper = jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper';
223
+ if (isGripper) {
224
+ // Gripper: 0-100% -> 0-1
225
+ normalizedRatio = Math.max(0, Math.min(1, normalizedValue / 100));
226
+ } else {
227
+ // Regular joint: -100 to +100% -> 0-1
228
+ normalizedRatio = Math.max(0, Math.min(1, (normalizedValue + 100) / 200));
229
+ }
230
+
231
+ return Math.round(minServoValue + normalizedRatio * range);
232
+ }
233
+
234
+ // Register callback for calibration completion with final positions
235
+ onCalibrationCompleteWithPositions(callback: (positions: Record<string, number>) => void): () => void {
236
+ this.completionCallbacks.push(callback);
237
+
238
+ // Return unsubscribe function
239
+ return () => {
240
+ const index = this.completionCallbacks.indexOf(callback);
241
+ if (index >= 0) {
242
+ this.completionCallbacks.splice(index, 1);
243
+ }
244
+ };
245
+ }
246
+
247
+ // Update progress based on calibration coverage
248
+ private updateProgress(): void {
249
+ if (!this.isCalibrating) return;
250
+
251
+ let totalProgress = 0;
252
+ let jointCount = 0;
253
+
254
+ Object.values(this.jointCalibrations).forEach(calibration => {
255
+ jointCount++;
256
+ if (calibration.minServoValue !== undefined && calibration.maxServoValue !== undefined) {
257
+ const range = calibration.maxServoValue - calibration.minServoValue;
258
+ // Progress is based on range size (more range = more progress)
259
+ const jointProgress = Math.min(100, (range / ROBOT_CONFIG.calibration.minRangeThreshold) * 100);
260
+ totalProgress += jointProgress;
261
+ }
262
+ });
263
+
264
+ this.progress = jointCount > 0 ? totalProgress / jointCount : 0;
265
+ }
266
+
267
+ // Cleanup
268
+ destroy(): void {
269
+ this.completionCallbacks = [];
270
  }
271
  }
src/lib/elements/robot/calibration/USBCalibrationManager.ts DELETED
@@ -1,544 +0,0 @@
1
- import type { JointCalibration, CalibrationState } from '../models.js';
2
- import { scsServoSDK } from "feetech.js";
3
- import { ROBOT_CONFIG } from '../config.js';
4
-
5
- export class USBCalibrationManager {
6
- // Joint configuration
7
- private readonly jointIds = [1, 2, 3, 4, 5, 6];
8
- private readonly jointNames = ["Rotation", "Pitch", "Elbow", "Wrist_Pitch", "Wrist_Roll", "Jaw"];
9
-
10
- // Calibration state
11
- private jointCalibrations: Record<string, JointCalibration> = {};
12
- private _calibrationState: CalibrationState = {
13
- isCalibrating: false,
14
- progress: 0
15
- };
16
-
17
- // Connection state for calibration
18
- private isConnectedForCalibration = false;
19
- private baudRate: number = 1000000;
20
-
21
- // Calibration polling
22
- private calibrationPollingAbortController: AbortController | null = null;
23
- private lastPositions: Record<string, number> = {};
24
- private calibrationCallbacks: (() => void)[] = [];
25
-
26
- // Calibration completion callback with final positions
27
- private calibrationCompleteCallback: ((finalPositions: Record<string, number>) => void) | null = null;
28
-
29
- // Servo reading queue for calibration
30
- private isReadingServos = false;
31
- private readingQueue: Array<{
32
- servoId: number;
33
- resolve: (value: number) => void;
34
- reject: (error: Error) => void;
35
- }> = [];
36
-
37
- constructor(baudRate: number = ROBOT_CONFIG.usb.baudRate) {
38
- this.baudRate = baudRate;
39
-
40
- // Initialize joint calibrations
41
- this.jointNames.forEach(name => {
42
- this.jointCalibrations[name] = { isCalibrated: false };
43
- });
44
- }
45
-
46
- // Getters
47
- get isCalibrated(): boolean {
48
- return Object.values(this.jointCalibrations).every(cal => cal.isCalibrated);
49
- }
50
-
51
- get needsCalibration(): boolean {
52
- return !this.isCalibrated;
53
- }
54
-
55
- get calibrationState(): CalibrationState {
56
- return this._calibrationState;
57
- }
58
-
59
- get jointNames_(): string[] {
60
- return [...this.jointNames];
61
- }
62
-
63
- // Connection management for calibration
64
- async ensureConnectedForCalibration(): Promise<void> {
65
- if (this.isConnectedForCalibration) {
66
- console.log('[USBCalibrationManager] Already connected for calibration');
67
- return;
68
- }
69
-
70
- try {
71
- console.log('[USBCalibrationManager] Connecting SDK for calibration...');
72
- await scsServoSDK.connect({ baudRate: this.baudRate });
73
- this.isConnectedForCalibration = true;
74
- console.log('[USBCalibrationManager] Connected successfully for calibration');
75
- } catch (error) {
76
- console.error('[USBCalibrationManager] Failed to connect SDK for calibration:', error);
77
- throw error;
78
- }
79
- }
80
-
81
- async disconnectFromCalibration(): Promise<void> {
82
- if (!this.isConnectedForCalibration) return;
83
-
84
- try {
85
- await scsServoSDK.disconnect();
86
- this.isConnectedForCalibration = false;
87
- console.log('[USBCalibrationManager] Disconnected from calibration');
88
- } catch (error) {
89
- console.warn('[USBCalibrationManager] Failed to disconnect from calibration:', error);
90
- }
91
- }
92
-
93
- // Check if the SDK is currently connected (for external use)
94
- get isSDKConnected(): boolean {
95
- return this.isConnectedForCalibration && scsServoSDK.isConnected();
96
- }
97
-
98
- // Calibration methods
99
- async startCalibration(): Promise<void> {
100
- if (this._calibrationState.isCalibrating) {
101
- console.warn('[USBCalibrationManager] Calibration already in progress');
102
- return;
103
- }
104
-
105
- console.log('[USBCalibrationManager] Starting calibration process');
106
-
107
- // Ensure connection for calibration
108
- await this.ensureConnectedForCalibration();
109
-
110
- // Unlock all servos for calibration (allow manual movement)
111
- console.log('[USBCalibrationManager] 🔓 Unlocking all servos for calibration...');
112
- try {
113
- await scsServoSDK.unlockServos(this.jointIds);
114
- console.log('[USBCalibrationManager] ✅ All servos unlocked for manual movement during calibration');
115
- } catch (error) {
116
- console.warn('[USBCalibrationManager] Warning: Failed to unlock some servos for calibration:', error);
117
- }
118
-
119
- this._calibrationState = {
120
- isCalibrating: true,
121
- progress: 0
122
- };
123
-
124
- // Initialize calibrations with current values
125
- this.jointCalibrations = {};
126
- this.jointNames.forEach(name => {
127
- const currentValue = this.lastPositions[name] || 2048;
128
- this.jointCalibrations[name] = {
129
- isCalibrated: false,
130
- minServoValue: currentValue,
131
- maxServoValue: currentValue
132
- };
133
- });
134
-
135
- this.startCalibrationPolling();
136
- this.notifyCalibrationChange();
137
- }
138
-
139
- async stopCalibration(): Promise<void> {
140
- console.log('[USBCalibrationManager] Stopping calibration');
141
-
142
- this._calibrationState = {
143
- isCalibrating: false,
144
- progress: 100
145
- };
146
-
147
- // Mark all joints as calibrated
148
- this.jointNames.forEach(name => {
149
- if (this.jointCalibrations[name]) {
150
- this.jointCalibrations[name].isCalibrated = true;
151
- }
152
- });
153
-
154
- this.stopCalibrationPolling();
155
-
156
- // NEW: Read final positions and sync to virtual robot before locking
157
- console.log('[USBCalibrationManager] 📍 Reading final servo positions for virtual robot sync...');
158
- try {
159
- const finalPositions = await this.readFinalPositionsAndSync();
160
- console.log('[USBCalibrationManager] ✅ Final positions read and synced to virtual robot');
161
-
162
- // Notify robot of calibration completion with final positions
163
- if (this.calibrationCompleteCallback) {
164
- this.calibrationCompleteCallback(finalPositions);
165
- }
166
- } catch (error) {
167
- console.error('[USBCalibrationManager] Failed to read final positions:', error);
168
- }
169
-
170
- this.notifyCalibrationChange();
171
-
172
- // Keep connection open - don't disconnect automatically
173
- // The connection will be reused by USB drivers
174
- console.log('[USBCalibrationManager] Calibration complete, keeping connection for drivers');
175
- }
176
-
177
- skipCalibration(): void {
178
- console.log('[USBCalibrationManager] Skipping calibration, using full range');
179
-
180
- // Set full range for all joints
181
- this.jointNames.forEach(name => {
182
- this.jointCalibrations[name] = {
183
- isCalibrated: true,
184
- minServoValue: 0,
185
- maxServoValue: 4095
186
- };
187
- });
188
-
189
- this._calibrationState = {
190
- isCalibrating: false,
191
- progress: 100
192
- };
193
-
194
- this.notifyCalibrationChange();
195
- }
196
-
197
- // NEW: Set predefined calibration values
198
- async setPredefinedCalibration(): Promise<void> {
199
- console.log('[USBCalibrationManager] Setting predefined calibration values');
200
-
201
- // Ensure SDK connection for hardware access
202
- await this.ensureConnectedForCalibration();
203
-
204
- // Predefined calibration values based on known good robot configuration
205
- const predefinedValues: Record<string, { min: number; max: number; current: number }> = {
206
- "Rotation": { current: 2180, min: 764, max: 3388 },
207
- "Pitch": { current: 1159, min: 1138, max: 3501 },
208
- "Elbow": { current: 2874, min: 660, max: 2876 },
209
- "Wrist_Pitch": { current: 2138, min: 762, max: 3075 },
210
- "Wrist_Roll": { current: 2081, min: 154, max: 3995 },
211
- "Jaw": { current: 2061, min: 2013, max: 3555 }
212
- };
213
-
214
- // Set calibration values for all joints
215
- this.jointNames.forEach(name => {
216
- const values = predefinedValues[name];
217
- if (values) {
218
- this.jointCalibrations[name] = {
219
- isCalibrated: true,
220
- minServoValue: values.min,
221
- maxServoValue: values.max
222
- };
223
- // Set current position for reference
224
- this.lastPositions[name] = values.current;
225
- }
226
- });
227
-
228
- this._calibrationState = {
229
- isCalibrating: false,
230
- progress: 100
231
- };
232
-
233
- this.notifyCalibrationChange();
234
- console.log('[USBCalibrationManager] Predefined calibration values applied successfully');
235
- }
236
-
237
- // NEW: Read final positions and prepare for sync
238
- private async readFinalPositionsAndSync(): Promise<Record<string, number>> {
239
- const finalPositions: Record<string, number> = {};
240
-
241
- console.log('[USBCalibrationManager] Reading final positions from all servos...');
242
-
243
- // Read all servo positions sequentially
244
- for (let i = 0; i < this.jointIds.length; i++) {
245
- const servoId = this.jointIds[i];
246
- const jointName = this.jointNames[i];
247
-
248
- try {
249
- const position = await this.readServoPosition(servoId);
250
- finalPositions[jointName] = position;
251
- this.lastPositions[jointName] = position;
252
-
253
- console.log(`[USBCalibrationManager] ${jointName} (servo ${servoId}): ${position} (raw) -> ${this.normalizeValue(position, jointName).toFixed(1)}% (normalized)`);
254
- } catch (error) {
255
- console.warn(`[USBCalibrationManager] Failed to read final position for ${jointName} (servo ${servoId}):`, error);
256
- // Use last known position as fallback
257
- finalPositions[jointName] = this.lastPositions[jointName] || 2048;
258
- }
259
- }
260
-
261
- return finalPositions;
262
- }
263
-
264
- // NEW: Set callback for calibration completion with final positions
265
- onCalibrationCompleteWithPositions(callback: (finalPositions: Record<string, number>) => void): () => void {
266
- this.calibrationCompleteCallback = callback;
267
- return () => {
268
- this.calibrationCompleteCallback = null;
269
- };
270
- }
271
-
272
- // Post-calibration servo locking methods
273
- async lockServosForProduction(): Promise<void> {
274
- if (!this.isSDKConnected) {
275
- throw new Error('SDK not connected - cannot lock servos');
276
- }
277
-
278
- console.log('[USBCalibrationManager] 🔒 Locking all servos for production use (robot control)...');
279
- try {
280
- // Use the new lockServosForProduction function that both locks and enables torque
281
- await scsServoSDK.lockServosForProduction(this.jointIds);
282
- console.log('[USBCalibrationManager] ✅ All servos locked for production - robot is now controlled and cannot be moved manually');
283
- } catch (error) {
284
- console.error('[USBCalibrationManager] Failed to lock servos for production:', error);
285
- throw error;
286
- }
287
- }
288
-
289
- async keepServosUnlockedForConsumer(): Promise<void> {
290
- if (!this.isSDKConnected) {
291
- console.log('[USBCalibrationManager] SDK not connected - servos remain in current state');
292
- return;
293
- }
294
-
295
- console.log('[USBCalibrationManager] 🔓 Keeping servos unlocked for consumer use (reading positions)...');
296
- try {
297
- // Ensure servos are unlocked for reading
298
- await scsServoSDK.unlockServos(this.jointIds);
299
- console.log('[USBCalibrationManager] ✅ All servos remain unlocked for consumer - can be moved manually and positions read');
300
- } catch (error) {
301
- console.warn('[USBCalibrationManager] Warning: Failed to ensure servos are unlocked for consumer:', error);
302
- }
303
- }
304
-
305
- // Data access methods
306
- getCurrentRawValue(jointName: string): number | undefined {
307
- return this.lastPositions[jointName];
308
- }
309
-
310
- getJointCalibration(jointName: string): JointCalibration | undefined {
311
- return this.jointCalibrations[jointName];
312
- }
313
-
314
- getJointRange(jointName: string): number {
315
- const calibration = this.jointCalibrations[jointName];
316
- if (!calibration?.minServoValue || !calibration?.maxServoValue) return 0;
317
- return Math.abs(calibration.maxServoValue - calibration.minServoValue);
318
- }
319
-
320
- getAllCalibrations(): Record<string, JointCalibration> {
321
- return { ...this.jointCalibrations };
322
- }
323
-
324
- // Value conversion methods
325
- normalizeValue(servoValue: number, jointName: string): number {
326
- const calibration = this.jointCalibrations[jointName];
327
- const isGripper = jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper';
328
-
329
- if (!calibration?.isCalibrated || !calibration.minServoValue || !calibration.maxServoValue) {
330
- if (isGripper) {
331
- return (servoValue / 4095) * 100;
332
- } else {
333
- return ((servoValue - 2048) / 2048) * 100;
334
- }
335
- }
336
-
337
- const { minServoValue, maxServoValue } = calibration;
338
- if (maxServoValue === minServoValue) return 0;
339
-
340
- const bounded = Math.max(minServoValue, Math.min(maxServoValue, servoValue));
341
-
342
- if (isGripper) {
343
- return ((bounded - minServoValue) / (maxServoValue - minServoValue)) * 100;
344
- } else {
345
- return (((bounded - minServoValue) / (maxServoValue - minServoValue)) * 200) - 100;
346
- }
347
- }
348
-
349
- denormalizeValue(normalizedValue: number, jointName: string): number {
350
- const calibration = this.jointCalibrations[jointName];
351
- const isGripper = jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper';
352
-
353
- if (!calibration?.isCalibrated || !calibration.minServoValue || !calibration.maxServoValue) {
354
- // No calibration, use appropriate default conversion
355
- if (isGripper) {
356
- return Math.round((normalizedValue / 100) * 4095);
357
- } else {
358
- return Math.round(2048 + (normalizedValue / 100) * 2048);
359
- }
360
- }
361
-
362
- const { minServoValue, maxServoValue } = calibration;
363
- let ratio: number;
364
-
365
- if (isGripper) {
366
- ratio = normalizedValue / 100;
367
- } else {
368
- ratio = (normalizedValue + 100) / 200;
369
- }
370
-
371
- const servoValue = minServoValue + ratio * (maxServoValue - minServoValue);
372
- return Math.round(Math.max(minServoValue, Math.min(maxServoValue, servoValue)));
373
- }
374
-
375
- // Event handling
376
- onCalibrationChange(callback: () => void): () => void {
377
- this.calibrationCallbacks.push(callback);
378
- return () => {
379
- const index = this.calibrationCallbacks.indexOf(callback);
380
- if (index >= 0) {
381
- this.calibrationCallbacks.splice(index, 1);
382
- }
383
- };
384
- }
385
-
386
- // Format servo value for display
387
- formatServoValue(value: number | undefined): string {
388
- return value !== undefined ? value.toString() : '---';
389
- }
390
-
391
- // Cleanup
392
- async destroy(): Promise<void> {
393
- console.log('[USBCalibrationManager] 🧹 Destroying calibration manager...');
394
-
395
- this.stopCalibrationPolling();
396
-
397
- // Safely unlock all servos before disconnecting (best practice)
398
- if (this.isSDKConnected) {
399
- try {
400
- console.log('[USBCalibrationManager] 🔓 Safely unlocking all servos before cleanup...');
401
- await scsServoSDK.unlockServosForManualMovement(this.jointIds);
402
- console.log('[USBCalibrationManager] ✅ All servos safely unlocked for manual movement');
403
- } catch (error) {
404
- console.warn('[USBCalibrationManager] Warning: Failed to safely unlock servos during cleanup:', error);
405
- }
406
- }
407
-
408
- await this.disconnectFromCalibration();
409
- this.calibrationCallbacks = [];
410
- this.calibrationCompleteCallback = null;
411
-
412
- console.log('[USBCalibrationManager] ✅ Calibration manager destroyed');
413
- }
414
-
415
- // Private methods
416
- private async readServoPosition(servoId: number): Promise<number> {
417
- return new Promise((resolve, reject) => {
418
- this.readingQueue.push({ servoId, resolve, reject });
419
- this.processReadingQueue();
420
- });
421
- }
422
-
423
- private async processReadingQueue(): Promise<void> {
424
- if (this.isReadingServos || this.readingQueue.length === 0) {
425
- return;
426
- }
427
-
428
- this.isReadingServos = true;
429
-
430
- try {
431
- const batch = [...this.readingQueue];
432
- this.readingQueue = [];
433
-
434
- for (const { servoId, resolve, reject } of batch) {
435
- try {
436
- const position = await scsServoSDK.readPosition(servoId);
437
- resolve(position);
438
- } catch (error) {
439
- reject(error instanceof Error ? error : new Error(`Failed to read servo ${servoId}`));
440
- }
441
-
442
- await new Promise(resolve => setTimeout(resolve, 5));
443
- }
444
- } finally {
445
- this.isReadingServos = false;
446
-
447
- if (this.readingQueue.length > 0) {
448
- setTimeout(() => this.processReadingQueue(), 50);
449
- }
450
- }
451
- }
452
-
453
- private async startCalibrationPolling(): Promise<void> {
454
- this.stopCalibrationPolling();
455
-
456
- this.calibrationPollingAbortController = new AbortController();
457
- const signal = this.calibrationPollingAbortController.signal;
458
-
459
- console.log('[USBCalibrationManager] Starting calibration polling');
460
-
461
- try {
462
- while (!signal.aborted && this._calibrationState.isCalibrating) {
463
- const readPromises = this.jointIds.map(async (servoId, i) => {
464
- if (signal.aborted) return null;
465
-
466
- const jointName = this.jointNames[i];
467
-
468
- try {
469
- const currentValue = await this.readServoPosition(servoId);
470
- return { jointName, currentValue };
471
- } catch (error) {
472
- return null;
473
- }
474
- });
475
-
476
- const results = await Promise.all(readPromises);
477
- let hasUpdates = false;
478
-
479
- results.forEach(result => {
480
- if (!result) return;
481
-
482
- const { jointName, currentValue } = result;
483
- this.lastPositions[jointName] = currentValue;
484
-
485
- const calibration = this.jointCalibrations[jointName];
486
- if (calibration) {
487
- if (currentValue < calibration.minServoValue!) {
488
- calibration.minServoValue = currentValue;
489
- hasUpdates = true;
490
- }
491
- if (currentValue > calibration.maxServoValue!) {
492
- calibration.maxServoValue = currentValue;
493
- hasUpdates = true;
494
- }
495
- }
496
- });
497
-
498
- if (hasUpdates) {
499
- this.notifyCalibrationChange();
500
- }
501
-
502
- // Calculate progress
503
- const totalRangeNeeded = 500;
504
- let totalRangeDiscovered = 0;
505
-
506
- this.jointNames.forEach(name => {
507
- const calibration = this.jointCalibrations[name];
508
- if (calibration?.minServoValue !== undefined && calibration?.maxServoValue !== undefined) {
509
- totalRangeDiscovered += Math.abs(calibration.maxServoValue - calibration.minServoValue);
510
- }
511
- });
512
-
513
- const newProgress = Math.min(100, (totalRangeDiscovered / (totalRangeNeeded * this.jointNames.length)) * 100);
514
- if (Math.abs(newProgress - this._calibrationState.progress) > 1) {
515
- this._calibrationState.progress = newProgress;
516
- this.notifyCalibrationChange();
517
- }
518
-
519
- await new Promise(resolve => setTimeout(resolve, 10));
520
- }
521
- } catch (error) {
522
- if (!signal.aborted) {
523
- console.error('[USBCalibrationManager] Calibration polling error:', error);
524
- }
525
- }
526
- }
527
-
528
- private stopCalibrationPolling(): void {
529
- if (this.calibrationPollingAbortController) {
530
- this.calibrationPollingAbortController.abort();
531
- this.calibrationPollingAbortController = null;
532
- }
533
- }
534
-
535
- private notifyCalibrationChange(): void {
536
- this.calibrationCallbacks.forEach(callback => {
537
- try {
538
- callback();
539
- } catch (error) {
540
- console.error('[USBCalibrationManager] Error in calibration callback:', error);
541
- }
542
- });
543
- }
544
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/elements/robot/calibration/USBCalibrationPanel.svelte CHANGED
@@ -1,12 +1,10 @@
1
  <!-- USB Calibration Panel - Compact Modal Version -->
2
  <script lang="ts">
3
- import type { USBCalibrationManager } from "./USBCalibrationManager.js";
4
- import { CalibrationState } from "./CalibrationState.svelte.js";
5
  import { Button } from "@/components/ui/button/index.js";
6
  import { Badge } from "@/components/ui/badge/index.js";
7
 
8
  interface Props {
9
- calibrationManager: USBCalibrationManager;
10
  connectionType?: "consumer" | "producer";
11
  onCalibrationComplete?: () => void;
12
  onCancel?: () => void;
@@ -22,14 +20,11 @@
22
  // Joint names for reference
23
  const jointNames = ["Rotation", "Pitch", "Elbow", "Wrist_Pitch", "Wrist_Roll", "Jaw"];
24
 
25
- // Create reactive calibration state manager for high-performance updates
26
- const calibrationState = new CalibrationState(calibrationManager);
27
-
28
  // Reactive getters from the calibration state
29
- const isCalibrating = $derived(calibrationState.isCalibrating);
30
- const progress = $derived(calibrationState.progress);
31
- const isCalibrated = $derived(calibrationState.isCalibrated);
32
- const needsCalibration = $derived(calibrationState.needsCalibration);
33
 
34
  // Connection type descriptions
35
  const connectionInfo = $derived(
@@ -44,19 +39,13 @@
44
  }
45
  );
46
 
47
- // Cleanup on component destruction
48
- $effect(() => {
49
- return () => {
50
- calibrationState.destroy();
51
- };
52
- });
53
 
54
  async function startCalibration() {
55
  await calibrationManager.startCalibration();
56
  }
57
 
58
- async function stopCalibration() {
59
- await calibrationManager.stopCalibration();
60
  if (onCalibrationComplete) {
61
  onCalibrationComplete();
62
  }
@@ -83,7 +72,7 @@
83
 
84
  function handleCancel() {
85
  if (isCalibrating) {
86
- calibrationManager.stopCalibration();
87
  }
88
  if (onCancel) {
89
  onCancel();
@@ -102,7 +91,7 @@
102
  <Badge variant="secondary" class="text-xs">{Math.round(progress)}%</Badge>
103
  </div>
104
  <div class="flex gap-2">
105
- <Button variant="default" size="sm" onclick={stopCalibration}>Complete</Button>
106
  <Button variant="outline" size="sm" onclick={handleCancel}>Cancel</Button>
107
  </div>
108
  </div>
@@ -119,20 +108,20 @@
119
  <div class="max-h-48 overflow-y-auto">
120
  <div class="grid grid-cols-2 gap-2">
121
  {#each jointNames as jointName}
122
- {@const currentValue = calibrationState.getCurrentValue(jointName)}
123
- {@const calibration = calibrationState.getJointCalibration(jointName)}
124
 
125
  <div class="space-y-1 rounded bg-slate-700/50 p-2">
126
  <div class="flex items-center justify-between">
127
  <span class="text-xs font-medium text-slate-300">{jointName}</span>
128
  <span class="font-mono text-xs text-green-400"
129
- >{calibrationState.formatServoValue(currentValue)}</span
130
  >
131
  </div>
132
 
133
  <div class="flex justify-between text-xs text-slate-500">
134
- <span>Min: {calibrationState.formatServoValue(calibration?.minServoValue)}</span>
135
- <span>Max: {calibrationState.formatServoValue(calibration?.maxServoValue)}</span>
136
  </div>
137
 
138
  {#if calibration?.minServoValue !== undefined && calibration?.maxServoValue !== undefined && currentValue !== undefined}
@@ -178,8 +167,8 @@
178
  <div class="max-h-32 overflow-y-auto">
179
  <div class="grid grid-cols-2 gap-1">
180
  {#each jointNames as jointName}
181
- {@const calibration = calibrationState.getJointCalibration(jointName)}
182
- {@const range = calibrationState.getJointRange(jointName)}
183
 
184
  <div class="flex items-center justify-between rounded bg-slate-700/30 p-2 text-xs">
185
  <span class="font-medium text-slate-300">{jointName}</span>
 
1
  <!-- USB Calibration Panel - Compact Modal Version -->
2
  <script lang="ts">
 
 
3
  import { Button } from "@/components/ui/button/index.js";
4
  import { Badge } from "@/components/ui/badge/index.js";
5
 
6
  interface Props {
7
+ calibrationManager: any; // USBServoDriver (Consumer or Producer)
8
  connectionType?: "consumer" | "producer";
9
  onCalibrationComplete?: () => void;
10
  onCancel?: () => void;
 
20
  // Joint names for reference
21
  const jointNames = ["Rotation", "Pitch", "Elbow", "Wrist_Pitch", "Wrist_Roll", "Jaw"];
22
 
 
 
 
23
  // Reactive getters from the calibration state
24
+ const isCalibrating = $derived(calibrationManager.calibrationState.isCalibrating);
25
+ const progress = $derived(calibrationManager.calibrationState.progress);
26
+ const isCalibrated = $derived(calibrationManager.calibrationState.isCalibrated);
27
+ const needsCalibration = $derived(calibrationManager.calibrationState.needsCalibration);
28
 
29
  // Connection type descriptions
30
  const connectionInfo = $derived(
 
39
  }
40
  );
41
 
 
 
 
 
 
 
42
 
43
  async function startCalibration() {
44
  await calibrationManager.startCalibration();
45
  }
46
 
47
+ async function completeCalibration() {
48
+ await calibrationManager.completeCalibration();
49
  if (onCalibrationComplete) {
50
  onCalibrationComplete();
51
  }
 
72
 
73
  function handleCancel() {
74
  if (isCalibrating) {
75
+ calibrationManager.cancelCalibration();
76
  }
77
  if (onCancel) {
78
  onCancel();
 
91
  <Badge variant="secondary" class="text-xs">{Math.round(progress)}%</Badge>
92
  </div>
93
  <div class="flex gap-2">
94
+ <Button variant="default" size="sm" onclick={completeCalibration}>Complete</Button>
95
  <Button variant="outline" size="sm" onclick={handleCancel}>Cancel</Button>
96
  </div>
97
  </div>
 
108
  <div class="max-h-48 overflow-y-auto">
109
  <div class="grid grid-cols-2 gap-2">
110
  {#each jointNames as jointName}
111
+ {@const currentValue = calibrationManager.calibrationState.getCurrentValue(jointName)}
112
+ {@const calibration = calibrationManager.calibrationState.getJointCalibration(jointName)}
113
 
114
  <div class="space-y-1 rounded bg-slate-700/50 p-2">
115
  <div class="flex items-center justify-between">
116
  <span class="text-xs font-medium text-slate-300">{jointName}</span>
117
  <span class="font-mono text-xs text-green-400"
118
+ >{calibrationManager.calibrationState.formatServoValue(currentValue)}</span
119
  >
120
  </div>
121
 
122
  <div class="flex justify-between text-xs text-slate-500">
123
+ <span>Min: {calibrationManager.calibrationState.formatServoValue(calibration?.minServoValue)}</span>
124
+ <span>Max: {calibrationManager.calibrationState.formatServoValue(calibration?.maxServoValue)}</span>
125
  </div>
126
 
127
  {#if calibration?.minServoValue !== undefined && calibration?.maxServoValue !== undefined && currentValue !== undefined}
 
167
  <div class="max-h-32 overflow-y-auto">
168
  <div class="grid grid-cols-2 gap-1">
169
  {#each jointNames as jointName}
170
+ {@const calibration = calibrationManager.calibrationState.getJointCalibration(jointName)}
171
+ {@const range = calibrationManager.calibrationState.getJointRange(jointName)}
172
 
173
  <div class="flex items-center justify-between rounded bg-slate-700/30 p-2 text-xs">
174
  <span class="font-medium text-slate-300">{jointName}</span>
src/lib/elements/robot/calibration/index.ts CHANGED
@@ -1,4 +1,2 @@
1
- // Robot calibration exports
2
- export { USBCalibrationManager } from './USBCalibrationManager.js';
3
  export { CalibrationState } from './CalibrationState.svelte.js';
4
  export { default as USBCalibrationPanel } from './USBCalibrationPanel.svelte';
 
 
 
1
  export { CalibrationState } from './CalibrationState.svelte.js';
2
  export { default as USBCalibrationPanel } from './USBCalibrationPanel.svelte';
src/lib/elements/robot/components/ConnectionPanel.svelte CHANGED
@@ -39,30 +39,34 @@
39
  }
40
  });
41
 
42
- // Set up calibration completion callback with position sync
43
- robot.calibrationManager.onCalibrationCompleteWithPositions((finalPositions) => {
44
- console.log("[ConnectionPanel] Calibration complete, syncing robot to final positions");
45
- robot.syncToCalibrationPositions(finalPositions);
46
- });
 
 
 
 
47
 
48
  async function connectUSBConsumer() {
49
  try {
50
  connecting = true;
51
  error = null;
52
 
53
- // Check if calibration is needed
54
- if (robot.calibrationManager.needsCalibration) {
55
- pendingUSBConnection = "consumer";
56
- showUSBCalibration = true;
57
- return; // Don't proceed with connection yet
58
- }
59
-
60
  await robot.setConsumer({
61
  type: "usb",
62
  baudRate: 1000000
63
  });
64
  } catch (err) {
65
  console.error("Failed to connect USB consumer:", err);
 
 
 
 
 
 
66
  error = err instanceof Error ? err.message : "Unknown error";
67
  } finally {
68
  connecting = false;
@@ -92,19 +96,19 @@
92
  connecting = true;
93
  error = null;
94
 
95
- // Check if calibration is needed
96
- if (robot.calibrationManager.needsCalibration) {
97
- pendingUSBConnection = "producer";
98
- showUSBCalibration = true;
99
- return; // Don't proceed with connection yet
100
- }
101
-
102
  await robot.addProducer({
103
  type: "usb",
104
  baudRate: 1000000
105
  });
106
  } catch (err) {
107
  console.error("Failed to connect USB producer:", err);
 
 
 
 
 
 
108
  error = err instanceof Error ? err.message : "Unknown error";
109
  } finally {
110
  connecting = false;
@@ -533,12 +537,18 @@
533
  to software values.
534
  </div>
535
 
536
- <USBCalibrationPanel
537
- calibrationManager={robot.calibrationManager}
538
- connectionType={pendingUSBConnection || "consumer"}
539
- {onCalibrationComplete}
540
- onCancel={onCalibrationCancel}
541
- />
 
 
 
 
 
 
542
  </div>
543
  </div>
544
  {/if}
 
39
  }
40
  });
41
 
42
+ // Find USB driver for calibration (if any)
43
+ function getUSBDriver(): any {
44
+ // Check consumer first
45
+ if (robot.consumer && 'calibrationState' in robot.consumer) {
46
+ return robot.consumer;
47
+ }
48
+ // Then check producers
49
+ return robot.producers.find(p => 'calibrationState' in p) || null;
50
+ }
51
 
52
  async function connectUSBConsumer() {
53
  try {
54
  connecting = true;
55
  error = null;
56
 
57
+ // USB drivers handle their own calibration requirements
 
 
 
 
 
 
58
  await robot.setConsumer({
59
  type: "usb",
60
  baudRate: 1000000
61
  });
62
  } catch (err) {
63
  console.error("Failed to connect USB consumer:", err);
64
+ // Check if it's a calibration error
65
+ if (err instanceof Error && err.message.includes('calibration')) {
66
+ pendingUSBConnection = "consumer";
67
+ showUSBCalibration = true;
68
+ return;
69
+ }
70
  error = err instanceof Error ? err.message : "Unknown error";
71
  } finally {
72
  connecting = false;
 
96
  connecting = true;
97
  error = null;
98
 
99
+ // USB drivers handle their own calibration requirements
 
 
 
 
 
 
100
  await robot.addProducer({
101
  type: "usb",
102
  baudRate: 1000000
103
  });
104
  } catch (err) {
105
  console.error("Failed to connect USB producer:", err);
106
+ // Check if it's a calibration error
107
+ if (err instanceof Error && err.message.includes('calibration')) {
108
+ pendingUSBConnection = "producer";
109
+ showUSBCalibration = true;
110
+ return;
111
+ }
112
  error = err instanceof Error ? err.message : "Unknown error";
113
  } finally {
114
  connecting = false;
 
537
  to software values.
538
  </div>
539
 
540
+ {#if getUSBDriver()}
541
+ <USBCalibrationPanel
542
+ calibrationManager={getUSBDriver()}
543
+ connectionType={pendingUSBConnection || "consumer"}
544
+ {onCalibrationComplete}
545
+ onCancel={onCalibrationCancel}
546
+ />
547
+ {:else}
548
+ <div class="text-center text-slate-400">
549
+ No USB driver available for calibration
550
+ </div>
551
+ {/if}
552
  </div>
553
  </div>
554
  {/if}
src/lib/elements/robot/drivers/USBConsumer.ts CHANGED
@@ -1,144 +1,70 @@
1
- import type { Consumer, ConnectionStatus, RobotCommand, USBDriverConfig } from '../models.js';
2
- import { USBCalibrationManager } from '../calibration/USBCalibrationManager.js';
3
- import { scsServoSDK } from "feetech.js";
4
  import { ROBOT_CONFIG } from '../config.js';
5
 
6
- export class USBConsumer implements Consumer {
7
- readonly id: string;
8
- readonly name = 'USB Consumer';
9
- readonly config: USBDriverConfig;
10
-
11
- private _status: ConnectionStatus = { isConnected: false };
12
- private statusCallbacks: ((status: ConnectionStatus) => void)[] = [];
13
  private commandCallbacks: ((command: RobotCommand) => void)[] = [];
14
-
15
- // Listening state
16
- private isListening = false;
17
- private pollingAbortController: AbortController | null = null;
18
-
19
- // Joint configuration
20
- private readonly jointIds = [1, 2, 3, 4, 5, 6];
21
- private readonly jointNames = ["Rotation", "Pitch", "Elbow", "Wrist_Pitch", "Wrist_Roll", "Jaw"];
22
- private lastPositions: Record<string, number> = {};
23
-
24
- // Shared calibration manager
25
- private calibrationManager: USBCalibrationManager;
26
-
27
- // Servo reading queue
28
- private isReadingServos = false;
29
- private readingQueue: Array<{
30
- servoId: number;
31
- resolve: (value: number) => void;
32
- reject: (error: Error) => void;
33
- }> = [];
34
-
35
- // Error tracking for better backoff
36
- private consecutiveErrors = 0;
37
- private lastErrorTime = 0;
38
 
39
- constructor(config: USBDriverConfig, calibrationManager: USBCalibrationManager) {
40
- this.config = config;
41
- this.calibrationManager = calibrationManager;
42
- this.id = `usb-consumer-${Date.now()}`;
43
- }
44
-
45
- get status(): ConnectionStatus {
46
- return this._status;
47
  }
48
 
49
  async connect(): Promise<void> {
50
- if (this._status.isConnected) {
51
- console.debug('[USBConsumer] Already connected');
52
- return;
53
- }
54
-
55
- try {
56
- console.debug('[USBConsumer] Connecting...');
57
-
58
- // Check if calibration is needed
59
- if (this.calibrationManager.needsCalibration) {
60
- throw new Error('USB Consumer requires calibration. Please complete calibration first.');
61
- }
62
-
63
- // Ensure the SDK is connected (reuse calibration connection if available)
64
- if (!this.calibrationManager.isSDKConnected) {
65
- console.debug('[USBConsumer] Establishing new SDK connection');
66
- await scsServoSDK.connect({
67
- baudRate: this.config.baudRate || ROBOT_CONFIG.usb.baudRate
68
- });
69
- } else {
70
- console.debug('[USBConsumer] Reusing existing SDK connection from calibration');
71
- }
72
-
73
- // Ensure servos remain unlocked for consumer (reading positions)
74
- console.debug('[USBConsumer] 🔓 Ensuring servos remain unlocked for position reading...');
75
- await this.calibrationManager.keepServosUnlockedForConsumer();
76
-
77
- this._status = {
78
- isConnected: true,
79
- lastConnected: new Date()
80
- };
81
- this.notifyStatusChange();
82
-
83
- console.debug('[USBConsumer] ✅ Connected successfully - servos unlocked for reading');
84
- } catch (error) {
85
- console.error('[USBConsumer] Connection failed:', error);
86
- this._status = {
87
- isConnected: false,
88
- error: error instanceof Error ? error.message : 'Connection failed'
89
- };
90
- this.notifyStatusChange();
91
- throw error;
92
- }
93
  }
94
 
95
  async disconnect(): Promise<void> {
96
- if (this._status.isConnected) {
97
- await this.stopListening();
98
- console.debug('[USBConsumer] Disconnecting (keeping shared SDK connection)');
99
- // Don't disconnect the SDK here - let calibration manager handle it
100
- // This allows multiple USB drivers to share the same connection
101
- }
102
-
103
- this._status = { isConnected: false };
104
- this.notifyStatusChange();
105
  }
106
 
107
  async startListening(): Promise<void> {
108
- if (this.isListening) {
109
- console.warn('[USBConsumer] Already listening');
110
  return;
111
  }
112
 
113
- this.isListening = true;
114
- this.pollingAbortController = new AbortController();
 
 
 
 
115
 
116
- console.debug('[USBConsumer] Starting continuous polling');
117
- this.pollContinuously();
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  }
119
 
120
  async stopListening(): Promise<void> {
121
- if (!this.isListening) return;
122
-
123
- this.isListening = false;
124
- if (this.pollingAbortController) {
125
- this.pollingAbortController.abort();
126
- this.pollingAbortController = null;
127
  }
128
-
129
- console.debug('[USBConsumer] Stopped listening');
130
  }
131
 
132
- // Event handlers
133
- onStatusChange(callback: (status: ConnectionStatus) => void): () => void {
134
- this.statusCallbacks.push(callback);
135
- return () => {
136
- const index = this.statusCallbacks.indexOf(callback);
137
- if (index >= 0) {
138
- this.statusCallbacks.splice(index, 1);
139
- }
140
- };
141
- }
142
 
143
  onCommand(callback: (command: RobotCommand) => void): () => void {
144
  this.commandCallbacks.push(callback);
@@ -151,122 +77,63 @@ export class USBConsumer implements Consumer {
151
  }
152
 
153
  // Private methods
154
- private async readServoPosition(servoId: number): Promise<number> {
155
- return new Promise((resolve, reject) => {
156
- this.readingQueue.push({ servoId, resolve, reject });
157
- this.processReadingQueue();
158
- });
159
- }
160
-
161
- private async processReadingQueue(): Promise<void> {
162
- if (this.isReadingServos || this.readingQueue.length === 0) {
163
  return;
164
  }
165
 
166
- this.isReadingServos = true;
167
-
168
  try {
169
- const batch = [...this.readingQueue];
170
- this.readingQueue = [];
171
-
172
- for (const { servoId, resolve, reject } of batch) {
173
- try {
174
- const position = await scsServoSDK.readPosition(servoId);
175
- resolve(position);
176
- } catch (error) {
177
- reject(error instanceof Error ? error : new Error(`Failed to read servo ${servoId}`));
178
- }
179
-
180
- await new Promise(resolve => setTimeout(resolve, 5));
181
- }
182
- } finally {
183
- this.isReadingServos = false;
184
 
185
- if (this.readingQueue.length > 0) {
186
- setTimeout(() => this.processReadingQueue(), 50);
187
- }
188
- }
189
- }
190
-
191
- private async pollContinuously(): Promise<void> {
192
- while (this.isListening && this._status.isConnected && !this.pollingAbortController?.signal.aborted) {
193
- try {
194
- const changes: { name: string; value: number }[] = [];
195
-
196
- const readPromises = this.jointIds.map(async (servoId, i) => {
197
- const jointName = this.jointNames[i];
198
- try {
199
- const position = await this.readServoPosition(servoId);
200
- const lastPosition = this.lastPositions[jointName];
201
-
202
- if (position !== lastPosition) {
203
- // Use calibration manager for normalization
204
- const normalizedValue = this.calibrationManager.normalizeValue(position, jointName);
205
- this.lastPositions[jointName] = position;
206
- return { name: jointName, value: normalizedValue };
207
- }
208
- } catch (error) {
209
- // Silent continue on read errors
210
- }
211
- return null;
212
- });
213
-
214
- const results = await Promise.all(readPromises);
215
 
216
- results.forEach(result => {
217
- if (result) {
218
- changes.push(result);
219
- }
220
- });
221
-
222
- if (changes.length > 0) {
223
- const command: RobotCommand = {
224
- joints: changes,
225
- timestamp: Date.now()
226
- };
227
- this.notifyCommand(command);
228
 
229
- // Reset error counter on successful read
230
- this.consecutiveErrors = 0;
231
- }
232
-
233
- await new Promise(resolve => setTimeout(resolve, ROBOT_CONFIG.polling.consumerPollingRate));
234
-
235
- } catch (error) {
236
- if (!this.pollingAbortController?.signal.aborted) {
237
- console.error('[USBConsumer] Polling error:', error);
238
 
239
- // Smart error backoff
240
- this.consecutiveErrors++;
241
- this.lastErrorTime = Date.now();
242
 
243
- const backoffTime = this.consecutiveErrors > ROBOT_CONFIG.polling.maxPollingErrors
244
- ? ROBOT_CONFIG.polling.errorBackoffRate * 3 // Longer backoff after many errors
245
- : ROBOT_CONFIG.polling.errorBackoffRate;
246
-
247
- await new Promise(resolve => setTimeout(resolve, backoffTime));
248
  }
 
 
 
 
 
 
 
 
 
 
249
  }
 
 
 
250
  }
251
  }
252
 
253
- private notifyStatusChange(): void {
254
- this.statusCallbacks.forEach(callback => {
255
- try {
256
- callback(this._status);
257
- } catch (error) {
258
- console.error('[USBConsumer] Error in status callback:', error);
259
- }
260
- });
261
- }
262
-
263
  private notifyCommand(command: RobotCommand): void {
264
  this.commandCallbacks.forEach(callback => {
265
  try {
266
  callback(command);
267
  } catch (error) {
268
- console.error('[USBConsumer] Error in command callback:', error);
269
  }
270
  });
271
  }
 
 
272
  }
 
1
+ import type { Consumer, RobotCommand, ConnectionStatus } from '../models.js';
2
+ import { USBServoDriver } from './USBServoDriver.js';
 
3
  import { ROBOT_CONFIG } from '../config.js';
4
 
5
+ export class USBConsumer extends USBServoDriver implements Consumer {
 
 
 
 
 
 
6
  private commandCallbacks: ((command: RobotCommand) => void)[] = [];
7
+ private pollingInterval: ReturnType<typeof setInterval> | null = null;
8
+ private lastPositions: Record<number, number> = {};
9
+ private errorCount = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+ constructor(config: any) {
12
+ super(config, 'Consumer');
 
 
 
 
 
 
13
  }
14
 
15
  async connect(): Promise<void> {
16
+ // Connect to USB first (this triggers browser's device selection dialog)
17
+ await this.connectToUSB();
18
+
19
+ // Unlock servos for manual movement (consumer mode)
20
+ await this.unlockAllServos();
21
+
22
+ // Note: Calibration is checked when operations are actually needed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  }
24
 
25
  async disconnect(): Promise<void> {
26
+ await this.stopListening();
27
+ await this.disconnectFromUSB();
 
 
 
 
 
 
 
28
  }
29
 
30
  async startListening(): Promise<void> {
31
+ if (!this._status.isConnected || this.pollingInterval !== null) {
 
32
  return;
33
  }
34
 
35
+ if (!this.isCalibrated) {
36
+ throw new Error('Cannot start listening: not calibrated');
37
+ }
38
+
39
+ console.log(`[${this.name}] Starting position listening...`);
40
+ this.errorCount = 0;
41
 
42
+ this.pollingInterval = setInterval(async () => {
43
+ try {
44
+ await this.pollAndBroadcastPositions();
45
+ this.errorCount = 0;
46
+ } catch (error) {
47
+ this.errorCount++;
48
+ console.warn(`[${this.name}] Polling error (${this.errorCount}):`, error);
49
+
50
+ if (this.errorCount >= ROBOT_CONFIG.polling.maxPollingErrors) {
51
+ console.warn(`[${this.name}] Too many polling errors, slowing down...`);
52
+ await this.stopListening();
53
+ setTimeout(() => this.startListening(), ROBOT_CONFIG.polling.errorBackoffRate);
54
+ }
55
+ }
56
+ }, ROBOT_CONFIG.polling.consumerPollingRate);
57
  }
58
 
59
  async stopListening(): Promise<void> {
60
+ if (this.pollingInterval !== null) {
61
+ clearInterval(this.pollingInterval);
62
+ this.pollingInterval = null;
63
+ console.log(`[${this.name}] Stopped position listening`);
 
 
64
  }
 
 
65
  }
66
 
67
+ // Event handlers already in base class
 
 
 
 
 
 
 
 
 
68
 
69
  onCommand(callback: (command: RobotCommand) => void): () => void {
70
  this.commandCallbacks.push(callback);
 
77
  }
78
 
79
  // Private methods
80
+ private async pollAndBroadcastPositions(): Promise<void> {
81
+ if (!this.scsServoSDK || !this._status.isConnected) {
 
 
 
 
 
 
 
82
  return;
83
  }
84
 
 
 
85
  try {
86
+ // Read positions for all servos
87
+ const servoIds = Object.values(this.jointToServoMap);
88
+ const positions = await this.scsServoSDK.syncReadPositions(servoIds);
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
+ const jointsWithChanges: { name: string; value: number }[] = [];
91
+
92
+ // Check for position changes and convert to normalized values
93
+ Object.entries(this.jointToServoMap).forEach(([jointName, servoId]) => {
94
+ const currentPosition = positions.get(servoId);
95
+ const lastPosition = this.lastPositions[servoId];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
+ if (currentPosition !== undefined &&
98
+ (lastPosition === undefined ||
99
+ Math.abs(currentPosition - lastPosition) > ROBOT_CONFIG.performance.jointUpdateThreshold)) {
 
 
 
 
 
 
 
 
 
100
 
101
+ this.lastPositions[servoId] = currentPosition;
 
 
 
 
 
 
 
 
102
 
103
+ // Convert to normalized value using calibration (required)
104
+ const normalizedValue = this.normalizeValue(currentPosition, jointName);
 
105
 
106
+ jointsWithChanges.push({
107
+ name: jointName,
108
+ value: normalizedValue
109
+ });
 
110
  }
111
+ });
112
+
113
+ // Broadcast changes if any
114
+ if (jointsWithChanges.length > 0) {
115
+ const command: RobotCommand = {
116
+ timestamp: Date.now(),
117
+ joints: jointsWithChanges
118
+ };
119
+
120
+ this.notifyCommand(command);
121
  }
122
+
123
+ } catch (error) {
124
+ throw error; // Re-throw for error handling in polling loop
125
  }
126
  }
127
 
 
 
 
 
 
 
 
 
 
 
128
  private notifyCommand(command: RobotCommand): void {
129
  this.commandCallbacks.forEach(callback => {
130
  try {
131
  callback(command);
132
  } catch (error) {
133
+ console.error(`[${this.name}] Error in command callback:`, error);
134
  }
135
  });
136
  }
137
+
138
+
139
  }
src/lib/elements/robot/drivers/USBProducer.ts CHANGED
@@ -1,204 +1,122 @@
1
  import type { Producer, ConnectionStatus, RobotCommand, USBDriverConfig } from '../models.js';
2
- import { USBCalibrationManager } from '../calibration/USBCalibrationManager.js';
3
- import { scsServoSDK } from "feetech.js";
4
  import { ROBOT_CONFIG } from '../config.js';
 
5
 
6
- export class USBProducer implements Producer {
7
- readonly id: string;
8
- readonly name = 'USB Producer';
9
- readonly config: USBDriverConfig;
10
-
11
- private _status: ConnectionStatus = { isConnected: false };
12
- private statusCallbacks: ((status: ConnectionStatus) => void)[] = [];
13
-
14
- // Joint configuration
15
- private readonly jointIds = [1, 2, 3, 4, 5, 6];
16
- private readonly jointNames = ["Rotation", "Pitch", "Elbow", "Wrist_Pitch", "Wrist_Roll", "Jaw"];
17
-
18
- // Shared calibration manager
19
- private calibrationManager: USBCalibrationManager;
20
-
21
- // Serial command processing to prevent "Port is busy" errors
22
- private commandQueue: Array<{ joints: Array<{ name: string; value: number }>, resolve: () => void, reject: (error: Error) => void }> = [];
23
  private isProcessingCommands = false;
24
 
25
- constructor(config: USBDriverConfig, calibrationManager: USBCalibrationManager) {
26
- this.config = config;
27
- this.calibrationManager = calibrationManager;
28
- this.id = `usb-producer-${Date.now()}`;
29
- }
30
-
31
- get status(): ConnectionStatus {
32
- return this._status;
33
  }
34
 
35
  async connect(): Promise<void> {
36
- if (this._status.isConnected) {
37
- console.debug('[USBProducer] Already connected');
38
- return;
39
- }
40
-
41
- try {
42
- console.debug('[USBProducer] Connecting...');
43
-
44
- // Check if calibration is needed
45
- if (this.calibrationManager.needsCalibration) {
46
- throw new Error('USB Producer requires calibration. Please complete calibration first.');
47
- }
48
-
49
- // Ensure the SDK is connected (reuse calibration connection if available)
50
- if (!this.calibrationManager.isSDKConnected) {
51
- console.debug('[USBProducer] Establishing new SDK connection');
52
- await scsServoSDK.connect({
53
- baudRate: this.config.baudRate || ROBOT_CONFIG.usb.baudRate
54
- });
55
- } else {
56
- console.debug('[USBProducer] Reusing existing SDK connection from calibration');
57
- }
58
-
59
- // Lock servos for production use (robot control)
60
- console.debug('[USBProducer] 🔒 Locking servos for production use...');
61
- await this.calibrationManager.lockServosForProduction();
62
-
63
- this._status = {
64
- isConnected: true,
65
- lastConnected: new Date()
66
- };
67
- this.notifyStatusChange();
68
-
69
- console.debug('[USBProducer] ✅ Connected successfully - servos locked for robot control');
70
- } catch (error) {
71
- console.error('[USBProducer] Connection failed:', error);
72
- this._status = {
73
- isConnected: false,
74
- error: error instanceof Error ? error.message : 'Connection failed'
75
- };
76
- this.notifyStatusChange();
77
- throw error;
78
- }
79
  }
80
 
81
  async disconnect(): Promise<void> {
82
- if (this._status.isConnected) {
83
- console.debug('[USBProducer] 🔓 Disconnecting and unlocking servos...');
84
-
85
- try {
86
- // Safely unlock servos when disconnecting (best practice)
87
- if (this.calibrationManager.isSDKConnected) {
88
- console.debug('[USBProducer] 🔓 Safely unlocking servos for manual movement...');
89
- await scsServoSDK.unlockServosForManualMovement(this.jointIds);
90
- console.debug('[USBProducer] ✅ Servos safely unlocked - can now be moved manually');
91
- }
92
- } catch (error) {
93
- console.warn('[USBProducer] Warning: Failed to unlock servos during disconnect:', error);
94
- }
95
-
96
- // Don't disconnect the SDK here - let calibration manager handle it
97
- // This allows multiple USB drivers to share the same connection
98
- }
99
 
100
- this._status = { isConnected: false };
101
- this.notifyStatusChange();
102
- console.debug('[USBProducer] ✅ Disconnected');
103
  }
104
 
105
  async sendCommand(command: RobotCommand): Promise<void> {
106
  if (!this._status.isConnected) {
107
- throw new Error('Cannot send command: USB Producer not connected');
 
 
 
 
108
  }
109
 
110
- console.debug(`[USBProducer] Queuing command:`, command);
 
111
 
112
- // Queue command for serial processing
113
- return new Promise((resolve, reject) => {
114
- this.commandQueue.push({
115
- joints: command.joints,
116
- resolve,
117
- reject
118
- });
119
-
120
- // Start processing if not already running
121
  this.processCommandQueue();
122
- });
123
  }
124
 
125
- // Event handlers
126
- onStatusChange(callback: (status: ConnectionStatus) => void): () => void {
127
- this.statusCallbacks.push(callback);
128
- return () => {
129
- const index = this.statusCallbacks.indexOf(callback);
130
- if (index >= 0) {
131
- this.statusCallbacks.splice(index, 1);
132
- }
133
- };
134
- }
135
 
136
  // Private methods
137
  private async processCommandQueue(): Promise<void> {
138
- if (this.isProcessingCommands || this.commandQueue.length === 0) {
139
  return;
140
  }
141
 
142
  this.isProcessingCommands = true;
143
 
144
- try {
145
- while (this.commandQueue.length > 0) {
146
- const { joints, resolve, reject } = this.commandQueue.shift()!;
147
-
148
  try {
149
- // Process servos sequentially to prevent "Port is busy" errors
150
- for (const jointCmd of joints) {
151
- const jointIndex = this.jointNames.indexOf(jointCmd.name);
152
- if (jointIndex >= 0) {
153
- const servoId = this.jointIds[jointIndex];
154
- const servoPosition = this.calibrationManager.denormalizeValue(jointCmd.value, jointCmd.name);
155
-
156
- await this.writeServoWithRetry(servoId, servoPosition, jointCmd.name);
157
-
158
- // Small delay between servo writes to prevent port conflicts
159
- await new Promise(resolve => setTimeout(resolve, ROBOT_CONFIG.usb.servoWriteDelay));
160
- }
161
- }
162
-
163
- resolve();
164
  } catch (error) {
165
- reject(error as Error);
 
166
  }
167
  }
168
- } finally {
169
- this.isProcessingCommands = false;
170
  }
 
 
171
  }
172
 
173
- private async writeServoWithRetry(servoId: number, position: number, jointName: string): Promise<void> {
174
- let lastError: Error | null = null;
175
-
176
- for (let attempt = 1; attempt <= ROBOT_CONFIG.usb.maxRetries; attempt++) {
177
- try {
178
- await scsServoSDK.writePositionUnlocked(servoId, position);
179
- console.debug(`[USBProducer] ${jointName} (servo ${servoId}) -> ${position}`);
180
- return; // Success!
181
- } catch (error) {
182
- lastError = error as Error;
183
- console.warn(`[USBProducer] Attempt ${attempt}/${ROBOT_CONFIG.usb.maxRetries} failed for servo ${servoId}:`, error);
184
-
185
- if (attempt < ROBOT_CONFIG.usb.maxRetries) {
186
- await new Promise(resolve => setTimeout(resolve, ROBOT_CONFIG.usb.retryDelay));
 
 
 
187
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  }
 
 
 
 
189
  }
190
-
191
- // All retries failed
192
- throw new Error(`Failed to write servo ${servoId} after ${ROBOT_CONFIG.usb.maxRetries} attempts: ${lastError?.message}`);
193
  }
194
 
195
- private notifyStatusChange(): void {
196
- this.statusCallbacks.forEach(callback => {
197
- try {
198
- callback(this._status);
199
- } catch (error) {
200
- console.error('[USBProducer] Error in status callback:', error);
201
- }
202
- });
203
- }
204
  }
 
1
  import type { Producer, ConnectionStatus, RobotCommand, USBDriverConfig } from '../models.js';
 
 
2
  import { ROBOT_CONFIG } from '../config.js';
3
+ import { USBServoDriver } from './USBServoDriver.js';
4
 
5
+ export class USBProducer extends USBServoDriver implements Producer {
6
+ private commandQueue: RobotCommand[] = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  private isProcessingCommands = false;
8
 
9
+ constructor(config: any) {
10
+ super(config, 'Producer');
 
 
 
 
 
 
11
  }
12
 
13
  async connect(): Promise<void> {
14
+ // Connect to USB first (this triggers browser's device selection dialog)
15
+ await this.connectToUSB();
16
+
17
+ // Lock servos for software control (producer mode)
18
+ await this.lockAllServos();
19
+
20
+ // Note: Calibration is checked when operations are actually needed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  }
22
 
23
  async disconnect(): Promise<void> {
24
+ // Stop command processing
25
+ this.isProcessingCommands = false;
26
+ this.commandQueue = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
+ await this.disconnectFromUSB();
 
 
29
  }
30
 
31
  async sendCommand(command: RobotCommand): Promise<void> {
32
  if (!this._status.isConnected) {
33
+ throw new Error(`[${this.name}] Cannot send command: not connected`);
34
+ }
35
+
36
+ if (!this.isCalibrated) {
37
+ throw new Error(`[${this.name}] Cannot send command: not calibrated`);
38
  }
39
 
40
+ // Add command to queue for processing
41
+ this.commandQueue.push(command);
42
 
43
+ // Limit queue size to prevent memory issues
44
+ if (this.commandQueue.length > ROBOT_CONFIG.commands.maxQueueSize) {
45
+ this.commandQueue.shift(); // Remove oldest command
46
+ }
47
+
48
+ // Start processing if not already running
49
+ if (!this.isProcessingCommands) {
 
 
50
  this.processCommandQueue();
51
+ }
52
  }
53
 
54
+ // Event handlers already in base class
 
 
 
 
 
 
 
 
 
55
 
56
  // Private methods
57
  private async processCommandQueue(): Promise<void> {
58
+ if (this.isProcessingCommands || !this._status.isConnected) {
59
  return;
60
  }
61
 
62
  this.isProcessingCommands = true;
63
 
64
+ while (this.commandQueue.length > 0 && this._status.isConnected) {
65
+ const command = this.commandQueue.shift();
66
+ if (command) {
 
67
  try {
68
+ await this.executeCommand(command);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  } catch (error) {
70
+ console.error(`[${this.name}] Command execution failed:`, error);
71
+ // Continue processing other commands
72
  }
73
  }
 
 
74
  }
75
+
76
+ this.isProcessingCommands = false;
77
  }
78
 
79
+ private async executeCommand(command: RobotCommand): Promise<void> {
80
+ if (!this.scsServoSDK || !this._status.isConnected) {
81
+ return;
82
+ }
83
+
84
+ try {
85
+ // Convert normalized values to servo positions using calibration (required)
86
+ const servoCommands = new Map<number, number>();
87
+
88
+ command.joints.forEach(joint => {
89
+ const servoId = this.jointToServoMap[joint.name as keyof typeof this.jointToServoMap];
90
+ if (servoId) {
91
+ const servoPosition = this.denormalizeValue(joint.value, joint.name);
92
+
93
+ // Clamp to valid servo range
94
+ const clampedPosition = Math.max(0, Math.min(4095, Math.round(servoPosition)));
95
+ servoCommands.set(servoId, clampedPosition);
96
  }
97
+ });
98
+
99
+ if (servoCommands.size > 0) {
100
+ // Use batch commands when possible for better performance
101
+ if (servoCommands.size > 1) {
102
+ await this.scsServoSDK.syncWritePositions(servoCommands);
103
+ } else {
104
+ // Single servo command
105
+ const entry = servoCommands.entries().next().value;
106
+ if (entry) {
107
+ const [servoId, position] = entry;
108
+ await this.scsServoSDK.writePosition(servoId, position);
109
+ }
110
+ }
111
+
112
+ console.debug(`[${this.name}] Sent positions to ${servoCommands.size} servos`);
113
  }
114
+
115
+ } catch (error) {
116
+ console.error(`[${this.name}] Failed to execute command:`, error);
117
+ throw error;
118
  }
 
 
 
119
  }
120
 
121
+
 
 
 
 
 
 
 
 
122
  }
src/lib/elements/robot/drivers/index.ts CHANGED
@@ -1,4 +1,5 @@
1
  // Robot drivers exports
 
2
  export { USBConsumer } from './USBConsumer.js';
3
  export { USBProducer } from './USBProducer.js';
4
  export { RemoteConsumer } from './RemoteConsumer.js';
 
1
  // Robot drivers exports
2
+ export { USBServoDriver } from './USBServoDriver.js';
3
  export { USBConsumer } from './USBConsumer.js';
4
  export { USBProducer } from './USBProducer.js';
5
  export { RemoteConsumer } from './RemoteConsumer.js';
src/lib/elements/robot/index.ts CHANGED
@@ -12,7 +12,6 @@ export * from './models.js';
12
  export * from './drivers/index.js';
13
 
14
  // Robot calibration (avoid naming conflicts with models)
15
- export { USBCalibrationManager } from './calibration/USBCalibrationManager.js';
16
  export { CalibrationState as CalibrationStateManager } from './calibration/CalibrationState.svelte.js';
17
  export { default as USBCalibrationPanel } from './calibration/USBCalibrationPanel.svelte';
18
 
 
12
  export * from './drivers/index.js';
13
 
14
  // Robot calibration (avoid naming conflicts with models)
 
15
  export { CalibrationState as CalibrationStateManager } from './calibration/CalibrationState.svelte.js';
16
  export { default as USBCalibrationPanel } from './calibration/USBCalibrationPanel.svelte';
17