blanchon's picture
Update
6ce4ca6
raw
history blame
41.9 kB
import {
PortHandler,
PacketHandler,
COMM_SUCCESS,
COMM_RX_TIMEOUT,
COMM_RX_CORRUPT,
COMM_TX_FAIL,
COMM_NOT_AVAILABLE,
SCS_LOBYTE,
SCS_HIBYTE,
SCS_MAKEWORD,
GroupSyncRead, // Import GroupSyncRead
GroupSyncWrite // Import GroupSyncWrite
} from "./lowLevelSDK.mjs";
// Import address constants from the correct file
import {
ADDR_SCS_PRESENT_POSITION,
ADDR_SCS_GOAL_POSITION,
ADDR_SCS_TORQUE_ENABLE,
ADDR_SCS_GOAL_ACC,
ADDR_SCS_GOAL_SPEED
} from "./scsservo_constants.mjs";
// Import debug logging function
import { debugLog } from "./debug.mjs";
// Define constants not present in scsservo_constants.mjs
const ADDR_SCS_MODE = 33;
const ADDR_SCS_LOCK = 55;
const ADDR_SCS_ID = 5; // Address for Servo ID
const ADDR_SCS_BAUD_RATE = 6; // Address for Baud Rate
// Module-level variables for handlers
let portHandler = null;
let packetHandler = null;
/**
* Unified Servo SDK with flexible locking control
* Supports both locked (respects servo locks) and unlocked (temporary unlock) operations
*/
/**
* Connects to the serial port and initializes handlers.
* @param {object} [options] - Connection options.
* @param {number} [options.baudRate=1000000] - The baud rate for the serial connection.
* @param {number} [options.protocolEnd=0] - The protocol end setting (0 for STS/SMS, 1 for SCS).
* @returns {Promise<true>} Resolves with true on successful connection.
* @throws {Error} If connection fails or port cannot be opened/selected.
*/
export async function connect(options = {}) {
if (portHandler && portHandler.isOpen) {
debugLog("Already connected to servo system.");
return true;
}
const { baudRate = 1000000, protocolEnd = 0 } = options;
try {
portHandler = new PortHandler();
const portRequested = await portHandler.requestPort();
if (!portRequested) {
portHandler = null;
throw new Error("Failed to select a serial port.");
}
portHandler.setBaudRate(baudRate);
const portOpened = await portHandler.openPort();
if (!portOpened) {
await portHandler.closePort().catch(console.error);
portHandler = null;
throw new Error(`Failed to open port at baudrate ${baudRate}.`);
}
packetHandler = new PacketHandler(protocolEnd);
debugLog(`Connected to servo system at ${baudRate} baud, protocol end: ${protocolEnd}.`);
return true;
} catch (err) {
console.error("Error during servo connection:", err);
if (portHandler) {
try {
await portHandler.closePort();
} catch (closeErr) {
console.error("Error closing port after connection failure:", closeErr);
}
}
portHandler = null;
packetHandler = null;
throw new Error(`Servo connection failed: ${err.message}`);
}
}
/**
* Disconnects from the serial port.
* @returns {Promise<true>} Resolves with true on successful disconnection.
* @throws {Error} If disconnection fails.
*/
export async function disconnect() {
if (!portHandler || !portHandler.isOpen) {
debugLog("Already disconnected from servo system.");
return true;
}
try {
await portHandler.closePort();
portHandler = null;
packetHandler = null;
debugLog("Disconnected from servo system.");
return true;
} catch (err) {
console.error("Error during servo disconnection:", err);
portHandler = null;
packetHandler = null;
throw new Error(`Servo disconnection failed: ${err.message}`);
}
}
/**
* Checks if the SDK is currently connected.
* @returns {boolean} True if connected, false otherwise.
*/
export function isConnected() {
return !!(portHandler && portHandler.isOpen && packetHandler);
}
/**
* Checks if the SDK is connected. Throws an error if not.
* @throws {Error} If not connected.
*/
function checkConnection() {
if (!portHandler || !packetHandler) {
throw new Error("Not connected to servo system. Call connect() first.");
}
}
// =============================================================================
// SERVO LOCKING OPERATIONS
// =============================================================================
/**
* Locks a servo to prevent configuration changes.
* @param {number} servoId - The ID of the servo (1-252).
* @returns {Promise<"success">} Resolves with "success".
* @throws {Error} If not connected, write fails, or an exception occurs.
*/
export async function lockServo(servoId) {
checkConnection();
try {
debugLog(`🔒 Locking servo ${servoId}...`);
const [result, error] = await packetHandler.write1ByteTxRx(
portHandler,
servoId,
ADDR_SCS_LOCK,
1
);
if (result !== COMM_SUCCESS) {
throw new Error(
`Error locking servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error: ${error}`
);
}
debugLog(`🔒 Servo ${servoId} locked successfully`);
return "success";
} catch (err) {
console.error(`Exception locking servo ${servoId}:`, err);
throw new Error(`Failed to lock servo ${servoId}: ${err.message}`);
}
}
/**
* Unlocks a servo to allow configuration changes.
* @param {number} servoId - The ID of the servo (1-252).
* @returns {Promise<"success">} Resolves with "success".
* @throws {Error} If not connected, write fails, or an exception occurs.
*/
export async function unlockServo(servoId) {
checkConnection();
try {
debugLog(`🔓 Unlocking servo ${servoId}...`);
const [result, error] = await packetHandler.write1ByteTxRx(
portHandler,
servoId,
ADDR_SCS_LOCK,
0
);
if (result !== COMM_SUCCESS) {
throw new Error(
`Error unlocking servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error: ${error}`
);
}
debugLog(`🔓 Servo ${servoId} unlocked successfully`);
return "success";
} catch (err) {
console.error(`Exception unlocking servo ${servoId}:`, err);
throw new Error(`Failed to unlock servo ${servoId}: ${err.message}`);
}
}
/**
* Locks multiple servos sequentially.
* @param {number[]} servoIds - Array of servo IDs to lock.
* @returns {Promise<"success">} Resolves with "success".
* @throws {Error} If any servo fails to lock.
*/
export async function lockServos(servoIds) {
checkConnection();
debugLog(`🔒 Locking ${servoIds.length} servos: [${servoIds.join(', ')}]`);
// Lock servos sequentially to avoid port conflicts
for (const servoId of servoIds) {
await lockServo(servoId);
}
debugLog(`🔒 All ${servoIds.length} servos locked successfully`);
return "success";
}
/**
* Locks servos for production use by both locking configuration and enabling torque.
* This ensures servos are truly locked and controlled by the system.
* @param {number[]} servoIds - Array of servo IDs to lock for production.
* @returns {Promise<"success">} Resolves with "success".
* @throws {Error} If any servo fails to lock or enable torque.
*/
export async function lockServosForProduction(servoIds) {
checkConnection();
debugLog(`🔒 Locking ${servoIds.length} servos for production use: [${servoIds.join(', ')}]`);
// Lock servos sequentially and enable torque for each
for (const servoId of servoIds) {
try {
debugLog(`🔒 Locking servo ${servoId} for production...`);
// 1. Lock the servo configuration
const [lockResult, lockError] = await packetHandler.write1ByteTxRx(
portHandler,
servoId,
ADDR_SCS_LOCK,
1
);
if (lockResult !== COMM_SUCCESS) {
throw new Error(`Error locking servo ${servoId}: ${packetHandler.getTxRxResult(lockResult)}, Error: ${lockError}`);
}
// 2. Enable torque to make servo controllable
const [torqueResult, torqueError] = await packetHandler.write1ByteTxRx(
portHandler,
servoId,
ADDR_SCS_TORQUE_ENABLE,
1
);
if (torqueResult !== COMM_SUCCESS) {
console.warn(`Warning: Failed to enable torque for servo ${servoId}: ${packetHandler.getTxRxResult(torqueResult)}, Error: ${torqueError}`);
// Don't throw here, locking is more important than torque enable
}
debugLog(`🔒 Servo ${servoId} locked and torque enabled for production`);
} catch (err) {
console.error(`Exception locking servo ${servoId} for production:`, err);
throw new Error(`Failed to lock servo ${servoId} for production: ${err.message}`);
}
}
debugLog(`🔒 All ${servoIds.length} servos locked for production successfully`);
return "success";
}
/**
* Unlocks multiple servos sequentially.
* @param {number[]} servoIds - Array of servo IDs to unlock.
* @returns {Promise<"success">} Resolves with "success".
* @throws {Error} If any servo fails to unlock.
*/
export async function unlockServos(servoIds) {
checkConnection();
debugLog(`🔓 Unlocking ${servoIds.length} servos: [${servoIds.join(', ')}]`);
// Unlock servos sequentially to avoid port conflicts
for (const servoId of servoIds) {
await unlockServo(servoId);
}
debugLog(`🔓 All ${servoIds.length} servos unlocked successfully`);
return "success";
}
/**
* Safely unlocks servos for manual movement by unlocking configuration and disabling torque.
* This is the safest way to leave servos when disconnecting/cleaning up.
* @param {number[]} servoIds - Array of servo IDs to unlock safely.
* @returns {Promise<"success">} Resolves with "success".
* @throws {Error} If any servo fails to unlock or disable torque.
*/
export async function unlockServosForManualMovement(servoIds) {
checkConnection();
debugLog(`🔓 Safely unlocking ${servoIds.length} servos for manual movement: [${servoIds.join(', ')}]`);
// Unlock servos sequentially and disable torque for each
for (const servoId of servoIds) {
try {
debugLog(`🔓 Safely unlocking servo ${servoId} for manual movement...`);
// 1. Disable torque first (makes servo freely movable)
const [torqueResult, torqueError] = await packetHandler.write1ByteTxRx(
portHandler,
servoId,
ADDR_SCS_TORQUE_ENABLE,
0
);
if (torqueResult !== COMM_SUCCESS) {
console.warn(`Warning: Failed to disable torque for servo ${servoId}: ${packetHandler.getTxRxResult(torqueResult)}, Error: ${torqueError}`);
// Continue anyway, unlocking is more important
}
// 2. Unlock the servo configuration
const [unlockResult, unlockError] = await packetHandler.write1ByteTxRx(
portHandler,
servoId,
ADDR_SCS_LOCK,
0
);
if (unlockResult !== COMM_SUCCESS) {
throw new Error(`Error unlocking servo ${servoId}: ${packetHandler.getTxRxResult(unlockResult)}, Error: ${unlockError}`);
}
debugLog(`🔓 Servo ${servoId} safely unlocked - torque disabled and configuration unlocked`);
} catch (err) {
console.error(`Exception safely unlocking servo ${servoId}:`, err);
throw new Error(`Failed to safely unlock servo ${servoId}: ${err.message}`);
}
}
debugLog(`🔓 All ${servoIds.length} servos safely unlocked for manual movement`);
return "success";
}
// =============================================================================
// READ OPERATIONS (No locking needed)
// =============================================================================
/**
* Reads the current position of a servo.
* @param {number} servoId - The ID of the servo (1-252).
* @returns {Promise<number>} Resolves with the position (0-4095).
* @throws {Error} If not connected, read fails, or an exception occurs.
*/
export async function readPosition(servoId) {
checkConnection();
try {
const [position, result, error] = await packetHandler.read2ByteTxRx(
portHandler,
servoId,
ADDR_SCS_PRESENT_POSITION
);
if (result !== COMM_SUCCESS) {
throw new Error(
`Error reading position from servo ${servoId}: ${packetHandler.getTxRxResult(
result
)}, Error code: ${error}`
);
}
return position & 0xffff;
} catch (err) {
console.error(`Exception reading position from servo ${servoId}:`, err);
throw new Error(`Exception reading position from servo ${servoId}: ${err.message}`);
}
}
/**
* Reads the current baud rate index of a servo.
* @param {number} servoId - The ID of the servo (1-252).
* @returns {Promise<number>} Resolves with the baud rate index (0-7).
* @throws {Error} If not connected, read fails, or an exception occurs.
*/
export async function readBaudRate(servoId) {
checkConnection();
try {
const [baudIndex, result, error] = await packetHandler.read1ByteTxRx(
portHandler,
servoId,
ADDR_SCS_BAUD_RATE
);
if (result !== COMM_SUCCESS) {
throw new Error(
`Error reading baud rate from servo ${servoId}: ${packetHandler.getTxRxResult(
result
)}, Error code: ${error}`
);
}
return baudIndex;
} catch (err) {
console.error(`Exception reading baud rate from servo ${servoId}:`, err);
throw new Error(`Exception reading baud rate from servo ${servoId}: ${err.message}`);
}
}
/**
* Reads the current operating mode of a servo.
* @param {number} servoId - The ID of the servo (1-252).
* @returns {Promise<number>} Resolves with the mode (0 for position, 1 for wheel).
* @throws {Error} If not connected, read fails, or an exception occurs.
*/
export async function readMode(servoId) {
checkConnection();
try {
const [modeValue, result, error] = await packetHandler.read1ByteTxRx(
portHandler,
servoId,
ADDR_SCS_MODE
);
if (result !== COMM_SUCCESS) {
throw new Error(
`Error reading mode from servo ${servoId}: ${packetHandler.getTxRxResult(
result
)}, Error code: ${error}`
);
}
return modeValue;
} catch (err) {
console.error(`Exception reading mode from servo ${servoId}:`, err);
throw new Error(`Exception reading mode from servo ${servoId}: ${err.message}`);
}
}
/**
* Reads the current position of multiple servos synchronously.
* @param {number[]} servoIds - An array of servo IDs (1-252) to read from.
* @returns {Promise<Map<number, number>>} Resolves with a Map where keys are servo IDs and values are positions (0-4095).
* @throws {Error} If not connected or transmission fails completely.
*/
export async function syncReadPositions(servoIds) {
checkConnection();
if (!Array.isArray(servoIds) || servoIds.length === 0) {
debugLog("Sync Read: No servo IDs provided.");
return new Map();
}
const startAddress = ADDR_SCS_PRESENT_POSITION;
const dataLength = 2;
const groupSyncRead = new GroupSyncRead(portHandler, packetHandler, startAddress, dataLength);
const positions = new Map();
const validIds = [];
// Add parameters for each valid servo ID
servoIds.forEach((id) => {
if (id >= 1 && id <= 252) {
if (groupSyncRead.addParam(id)) {
validIds.push(id);
} else {
console.warn(`Sync Read: Failed to add param for servo ID ${id} (maybe duplicate or invalid).`);
}
} else {
console.warn(`Sync Read: Invalid servo ID ${id} skipped.`);
}
});
if (validIds.length === 0) {
debugLog("Sync Read: No valid servo IDs to read.");
return new Map();
}
try {
let txResult = await groupSyncRead.txPacket();
if (txResult !== COMM_SUCCESS) {
throw new Error(`Sync Read txPacket failed: ${packetHandler.getTxRxResult(txResult)}`);
}
let rxResult = await groupSyncRead.rxPacket();
if (rxResult !== COMM_SUCCESS) {
console.warn(`Sync Read rxPacket overall result: ${packetHandler.getTxRxResult(rxResult)}. Checking individual servos.`);
}
const failedIds = [];
validIds.forEach((id) => {
const isAvailable = groupSyncRead.isAvailable(id, startAddress, dataLength);
if (isAvailable) {
const position = groupSyncRead.getData(id, startAddress, dataLength);
positions.set(id, position & 0xffff);
} else {
failedIds.push(id);
}
});
if (failedIds.length > 0) {
console.warn(`Sync Read: Data not available for servo IDs: ${failedIds.join(", ")}. Got ${positions.size}/${validIds.length} servos successfully.`);
}
return positions;
} catch (err) {
console.error("Exception during syncReadPositions:", err);
throw new Error(`Sync Read failed: ${err.message}`);
}
}
// =============================================================================
// WRITE OPERATIONS - LOCKED MODE (Respects servo locks)
// =============================================================================
/**
* Writes a target position to a servo (respects locks).
* Will fail if the servo is locked.
* @param {number} servoId - The ID of the servo (1-252).
* @param {number} position - The target position value (0-4095).
* @returns {Promise<"success">} Resolves with "success".
* @throws {Error} If not connected, position is out of range, write fails, or an exception occurs.
*/
export async function writePosition(servoId, position) {
checkConnection();
try {
if (position < 0 || position > 4095) {
throw new Error(`Invalid position value ${position} for servo ${servoId}. Must be between 0 and 4095.`);
}
const targetPosition = Math.round(position);
const [result, error] = await packetHandler.write2ByteTxRx(
portHandler,
servoId,
ADDR_SCS_GOAL_POSITION,
targetPosition
);
if (result !== COMM_SUCCESS) {
throw new Error(`Error writing position to servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error code: ${error}`);
}
return "success";
} catch (err) {
console.error(`Exception writing position to servo ${servoId}:`, err);
throw new Error(`Failed to write position to servo ${servoId}: ${err.message}`);
}
}
/**
* Enables or disables the torque of a servo (respects locks).
* @param {number} servoId - The ID of the servo (1-252).
* @param {boolean} enable - True to enable torque, false to disable.
* @returns {Promise<"success">} Resolves with "success".
* @throws {Error} If not connected, write fails, or an exception occurs.
*/
export async function writeTorqueEnable(servoId, enable) {
checkConnection();
try {
const enableValue = enable ? 1 : 0;
const [result, error] = await packetHandler.write1ByteTxRx(
portHandler,
servoId,
ADDR_SCS_TORQUE_ENABLE,
enableValue
);
if (result !== COMM_SUCCESS) {
throw new Error(`Error setting torque for servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error code: ${error}`);
}
return "success";
} catch (err) {
console.error(`Exception setting torque for servo ${servoId}:`, err);
throw new Error(`Exception setting torque for servo ${servoId}: ${err.message}`);
}
}
// =============================================================================
// WRITE OPERATIONS - UNLOCKED MODE (Temporary unlock for operation)
// =============================================================================
/**
* Helper to attempt locking a servo, logging errors without throwing.
* @param {number} servoId
*/
async function tryLockServo(servoId) {
try {
await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1);
} catch (lockErr) {
console.error(`Failed to re-lock servo ${servoId}:`, lockErr);
}
}
/**
* Writes a target position to a servo with temporary unlocking.
* Temporarily unlocks the servo, writes the position, then locks it back.
* @param {number} servoId - The ID of the servo (1-252).
* @param {number} position - The target position value (0-4095).
* @returns {Promise<"success">} Resolves with "success".
* @throws {Error} If not connected, position is out of range, write fails, or an exception occurs.
*/
export async function writePositionUnlocked(servoId, position) {
checkConnection();
let unlocked = false;
try {
if (position < 0 || position > 4095) {
throw new Error(`Invalid position value ${position} for servo ${servoId}. Must be between 0 and 4095.`);
}
const targetPosition = Math.round(position);
debugLog(`🔓 Temporarily unlocking servo ${servoId} for position write...`);
// 1. Unlock servo configuration first
const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0);
if (resUnlock !== COMM_SUCCESS) {
debugLog(`Warning: Failed to unlock servo ${servoId}, trying direct write anyway...`);
} else {
unlocked = true;
}
// 2. Write the position
const [result, error] = await packetHandler.write2ByteTxRx(portHandler, servoId, ADDR_SCS_GOAL_POSITION, targetPosition);
if (result !== COMM_SUCCESS) {
throw new Error(`Error writing position to servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error code: ${error}`);
}
// 3. Lock servo configuration back
if (unlocked) {
const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1);
if (resLock !== COMM_SUCCESS) {
console.warn(`Warning: Failed to re-lock servo ${servoId} after position write: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`);
} else {
unlocked = false;
}
}
return "success";
} catch (err) {
console.error(`Exception writing position to servo ${servoId}:`, err);
if (unlocked) {
await tryLockServo(servoId);
}
throw new Error(`Failed to write position to servo ${servoId}: ${err.message}`);
}
}
/**
* Writes a target position and disables torque for manual movement.
* @param {number} servoId - The ID of the servo (1-252).
* @param {number} position - The target position value (0-4095).
* @param {number} waitTimeMs - Time to wait for servo to reach position (milliseconds).
* @returns {Promise<"success">} Resolves with "success".
* @throws {Error} If not connected, position is out of range, write fails, or an exception occurs.
*/
export async function writePositionAndDisableTorque(servoId, position, waitTimeMs = 1500) {
checkConnection();
let unlocked = false;
try {
if (position < 0 || position > 4095) {
throw new Error(`Invalid position value ${position} for servo ${servoId}. Must be between 0 and 4095.`);
}
const targetPosition = Math.round(position);
debugLog(`🔓 Moving servo ${servoId} to position ${targetPosition}, waiting ${waitTimeMs}ms, then disabling torque...`);
// 1. Unlock servo configuration first
const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0);
if (resUnlock !== COMM_SUCCESS) {
debugLog(`Warning: Failed to unlock servo ${servoId}, trying direct write anyway...`);
} else {
unlocked = true;
}
// 2. Enable torque first
const [torqueEnableResult, torqueEnableError] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_TORQUE_ENABLE, 1);
if (torqueEnableResult !== COMM_SUCCESS) {
console.warn(`Warning: Failed to enable torque for servo ${servoId}: ${packetHandler.getTxRxResult(torqueEnableResult)}, Error: ${torqueEnableError}`);
} else {
debugLog(`✅ Torque enabled for servo ${servoId}`);
}
// 3. Write the position
const [result, error] = await packetHandler.write2ByteTxRx(portHandler, servoId, ADDR_SCS_GOAL_POSITION, targetPosition);
if (result !== COMM_SUCCESS) {
throw new Error(`Error writing position to servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error code: ${error}`);
}
// 4. Wait for servo to reach position
debugLog(`⏳ Waiting ${waitTimeMs}ms for servo ${servoId} to reach position ${targetPosition}...`);
await new Promise(resolve => setTimeout(resolve, waitTimeMs));
// 5. Disable torque
const [torqueResult, torqueError] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_TORQUE_ENABLE, 0);
if (torqueResult !== COMM_SUCCESS) {
console.warn(`Warning: Failed to disable torque for servo ${servoId}: ${packetHandler.getTxRxResult(torqueResult)}, Error: ${torqueError}`);
} else {
debugLog(`✅ Torque disabled for servo ${servoId} - now movable by hand`);
}
// 6. Lock servo configuration back
if (unlocked) {
const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1);
if (resLock !== COMM_SUCCESS) {
console.warn(`Warning: Failed to re-lock servo ${servoId} after position write: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`);
} else {
unlocked = false;
}
}
return "success";
} catch (err) {
console.error(`Exception writing position and disabling torque for servo ${servoId}:`, err);
if (unlocked) {
await tryLockServo(servoId);
}
throw new Error(`Failed to write position and disable torque for servo ${servoId}: ${err.message}`);
}
}
/**
* Enables or disables the torque of a servo with temporary unlocking.
* @param {number} servoId - The ID of the servo (1-252).
* @param {boolean} enable - True to enable torque, false to disable.
* @returns {Promise<"success">} Resolves with "success".
* @throws {Error} If not connected, write fails, or an exception occurs.
*/
export async function writeTorqueEnableUnlocked(servoId, enable) {
checkConnection();
let unlocked = false;
try {
const enableValue = enable ? 1 : 0;
debugLog(`🔓 Temporarily unlocking servo ${servoId} for torque enable write...`);
// 1. Unlock servo configuration first
const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0);
if (resUnlock !== COMM_SUCCESS) {
debugLog(`Warning: Failed to unlock servo ${servoId}, trying direct write anyway...`);
} else {
unlocked = true;
}
// 2. Write the torque enable
const [result, error] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_TORQUE_ENABLE, enableValue);
if (result !== COMM_SUCCESS) {
throw new Error(`Error setting torque for servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error code: ${error}`);
}
// 3. Lock servo configuration back
if (unlocked) {
const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1);
if (resLock !== COMM_SUCCESS) {
console.warn(`Warning: Failed to re-lock servo ${servoId} after torque enable write: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`);
} else {
unlocked = false;
}
}
return "success";
} catch (err) {
console.error(`Exception setting torque for servo ${servoId}:`, err);
if (unlocked) {
await tryLockServo(servoId);
}
throw new Error(`Exception setting torque for servo ${servoId}: ${err.message}`);
}
}
// =============================================================================
// SYNC WRITE OPERATIONS
// =============================================================================
/**
* Writes target positions to multiple servos synchronously.
* @param {Map<number, number> | object} servoPositions - A Map or object where keys are servo IDs (1-252) and values are target positions (0-4095).
* @returns {Promise<"success">} Resolves with "success".
* @throws {Error} If not connected, any position is out of range, transmission fails, or an exception occurs.
*/
export async function syncWritePositions(servoPositions) {
checkConnection();
const groupSyncWrite = new GroupSyncWrite(portHandler, packetHandler, ADDR_SCS_GOAL_POSITION, 2);
let paramAdded = false;
const entries = servoPositions instanceof Map ? servoPositions.entries() : Object.entries(servoPositions);
for (const [idStr, position] of entries) {
const servoId = parseInt(idStr, 10);
if (isNaN(servoId) || servoId < 1 || servoId > 252) {
throw new Error(`Invalid servo ID "${idStr}" in syncWritePositions.`);
}
if (position < 0 || position > 4095) {
throw new Error(`Invalid position value ${position} for servo ${servoId} in syncWritePositions. Must be between 0 and 4095.`);
}
const targetPosition = Math.round(position);
const data = [SCS_LOBYTE(targetPosition), SCS_HIBYTE(targetPosition)];
if (groupSyncWrite.addParam(servoId, data)) {
paramAdded = true;
} else {
console.warn(`Failed to add servo ${servoId} to sync write group (possibly duplicate).`);
}
}
if (!paramAdded) {
debugLog("Sync Write: No valid servo positions provided or added.");
return "success";
}
try {
const result = await groupSyncWrite.txPacket();
if (result !== COMM_SUCCESS) {
throw new Error(`Sync Write txPacket failed: ${packetHandler.getTxRxResult(result)}`);
}
return "success";
} catch (err) {
console.error("Exception during syncWritePositions:", err);
throw new Error(`Sync Write failed: ${err.message}`);
}
}
/**
* Writes a target speed for a servo in wheel mode.
* @param {number} servoId - The ID of the servo
* @param {number} speed - The target speed value (-10000 to 10000). Negative values indicate reverse direction. 0 stops the wheel.
* @returns {Promise<"success">} Resolves with "success".
* @throws {Error} If not connected, either write fails, or an exception occurs.
*/
export async function writeWheelSpeed(servoId, speed) {
checkConnection();
let unlocked = false;
try {
const clampedSpeed = Math.max(-10000, Math.min(10000, Math.round(speed)));
let speedValue = Math.abs(clampedSpeed) & 0x7fff;
if (clampedSpeed < 0) {
speedValue |= 0x8000;
}
debugLog(`Temporarily unlocking servo ${servoId} for wheel speed write...`);
// 1. Unlock servo configuration first
const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0);
if (resUnlock !== COMM_SUCCESS) {
debugLog(`Warning: Failed to unlock servo ${servoId}, trying direct write anyway...`);
} else {
unlocked = true;
}
// 2. Write the speed
const [result, error] = await packetHandler.write2ByteTxRx(portHandler, servoId, ADDR_SCS_GOAL_SPEED, speedValue);
if (result !== COMM_SUCCESS) {
throw new Error(`Error writing wheel speed to servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error: ${error}`);
}
// 3. Lock servo configuration back
if (unlocked) {
const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1);
if (resLock !== COMM_SUCCESS) {
console.warn(`Warning: Failed to re-lock servo ${servoId} after wheel speed write: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`);
} else {
unlocked = false;
}
}
return "success";
} catch (err) {
console.error(`Exception writing wheel speed to servo ${servoId}:`, err);
if (unlocked) {
await tryLockServo(servoId);
}
throw new Error(`Exception writing wheel speed to servo ${servoId}: ${err.message}`);
}
}
/**
* Writes target speeds to multiple servos in wheel mode synchronously.
* @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).
* @returns {Promise<"success">} Resolves with "success".
* @throws {Error} If not connected, any speed is out of range, transmission fails, or an exception occurs.
*/
export async function syncWriteWheelSpeed(servoSpeeds) {
checkConnection();
const groupSyncWrite = new GroupSyncWrite(
portHandler,
packetHandler,
ADDR_SCS_GOAL_SPEED,
2 // Data length for speed (2 bytes)
);
let paramAdded = false;
const entries = servoSpeeds instanceof Map ? servoSpeeds.entries() : Object.entries(servoSpeeds);
// Second pass: Add valid parameters
for (const [idStr, speed] of entries) {
const servoId = parseInt(idStr, 10); // Already validated
if (isNaN(servoId) || servoId < 1 || servoId > 252) {
throw new Error(`Invalid servo ID "${idStr}" in syncWriteWheelSpeed.`);
}
if (speed < -10000 || speed > 10000) {
throw new Error(
`Invalid speed value ${speed} for servo ${servoId} in syncWriteWheelSpeed. Must be between -10000 and 10000.`
);
}
const clampedSpeed = Math.max(-10000, Math.min(10000, Math.round(speed))); // Ensure integer, already validated range
let speedValue = Math.abs(clampedSpeed) & 0x7fff; // Get absolute value, ensure within 15 bits
// Set the direction bit (MSB of the 16-bit value) if speed is negative
if (clampedSpeed < 0) {
speedValue |= 0x8000; // Set the 16th bit for reverse direction
}
const data = [SCS_LOBYTE(speedValue), SCS_HIBYTE(speedValue)];
if (groupSyncWrite.addParam(servoId, data)) {
paramAdded = true;
} else {
// This should ideally not happen if IDs are unique, but handle defensively
console.warn(
`Failed to add servo ${servoId} to sync write speed group (possibly duplicate).`
);
}
}
if (!paramAdded) {
debugLog("Sync Write Speed: No valid servo speeds provided or added.");
return "success"; // Nothing to write is considered success
}
try {
// Send the Sync Write instruction
const result = await groupSyncWrite.txPacket();
if (result !== COMM_SUCCESS) {
throw new Error(`Sync Write Speed txPacket failed: ${packetHandler.getTxRxResult(result)}`);
}
return "success";
} catch (err) {
console.error("Exception during syncWriteWheelSpeed:", err);
// Re-throw the original error or a new one wrapping it
throw new Error(`Sync Write Speed failed: ${err.message}`);
}
}
/**
* Sets the Baud Rate of a servo.
* NOTE: After changing the baud rate, you might need to disconnect and reconnect
* at the new baud rate to communicate with the servo further.
* @param {number} servoId - The current ID of the servo to configure (1-252).
* @param {number} baudRateIndex - The index representing the new baud rate (0-7).
* @returns {Promise<"success">} Resolves with "success".
* @throws {Error} If not connected, input is invalid, any step fails, or an exception occurs.
*/
export async function setBaudRate(servoId, baudRateIndex) {
checkConnection();
// Validate inputs
if (servoId < 1 || servoId > 252) {
throw new Error(`Invalid servo ID provided: ${servoId}. Must be between 1 and 252.`);
}
if (baudRateIndex < 0 || baudRateIndex > 7) {
throw new Error(`Invalid baudRateIndex: ${baudRateIndex}. Must be between 0 and 7.`);
}
let unlocked = false;
try {
debugLog(`Setting baud rate for servo ${servoId}: Index=${baudRateIndex}`);
// 1. Unlock servo configuration
const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(
portHandler,
servoId,
ADDR_SCS_LOCK,
0 // 0 to unlock
);
if (resUnlock !== COMM_SUCCESS) {
throw new Error(
`Failed to unlock servo ${servoId}: ${packetHandler.getTxRxResult(
resUnlock
)}, Error: ${errUnlock}`
);
}
unlocked = true;
// 2. Write new Baud Rate index
const [resBaud, errBaud] = await packetHandler.write1ByteTxRx(
portHandler,
servoId,
ADDR_SCS_BAUD_RATE,
baudRateIndex
);
if (resBaud !== COMM_SUCCESS) {
throw new Error(
`Failed to write baud rate index ${baudRateIndex} to servo ${servoId}: ${packetHandler.getTxRxResult(
resBaud
)}, Error: ${errBaud}`
);
}
// 3. Lock servo configuration
const [resLock, errLock] = await packetHandler.write1ByteTxRx(
portHandler,
servoId,
ADDR_SCS_LOCK,
1
);
if (resLock !== COMM_SUCCESS) {
throw new Error(
`Failed to lock servo ${servoId} after setting baud rate: ${packetHandler.getTxRxResult(
resLock
)}, Error: ${errLock}.`
);
}
unlocked = false; // Successfully locked
debugLog(
`Successfully set baud rate for servo ${servoId}. Index: ${baudRateIndex}. Remember to potentially reconnect with the new baud rate.`
);
return "success";
} catch (err) {
console.error(`Exception during setBaudRate for servo ID ${servoId}:`, err);
if (unlocked) {
await tryLockServo(servoId);
}
throw new Error(`Failed to set baud rate for servo ${servoId}: ${err.message}`);
}
}
/**
* Sets the ID of a servo.
* NOTE: Changing the ID requires using the new ID for subsequent commands.
* @param {number} currentServoId - The current ID of the servo to configure (1-252).
* @param {number} newServoId - The new ID to set for the servo (1-252).
* @returns {Promise<"success">} Resolves with "success".
* @throws {Error} If not connected, input is invalid, any step fails, or an exception occurs.
*/
export async function setServoId(currentServoId, newServoId) {
checkConnection();
// Validate inputs
if (currentServoId < 1 || currentServoId > 252 || newServoId < 1 || newServoId > 252) {
throw new Error(
`Invalid servo ID provided. Current: ${currentServoId}, New: ${newServoId}. Must be between 1 and 252.`
);
}
if (currentServoId === newServoId) {
debugLog(`Servo ID is already ${newServoId}. No change needed.`);
return "success";
}
let unlocked = false;
let idWritten = false;
try {
debugLog(`Setting servo ID: From ${currentServoId} to ${newServoId}`);
// 1. Unlock servo configuration (using current ID)
const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(
portHandler,
currentServoId,
ADDR_SCS_LOCK,
0 // 0 to unlock
);
if (resUnlock !== COMM_SUCCESS) {
throw new Error(
`Failed to unlock servo ${currentServoId}: ${packetHandler.getTxRxResult(
resUnlock
)}, Error: ${errUnlock}`
);
}
unlocked = true;
// 2. Write new Servo ID (using current ID)
const [resId, errId] = await packetHandler.write1ByteTxRx(
portHandler,
currentServoId,
ADDR_SCS_ID,
newServoId
);
if (resId !== COMM_SUCCESS) {
throw new Error(
`Failed to write new ID ${newServoId} to servo ${currentServoId}: ${packetHandler.getTxRxResult(
resId
)}, Error: ${errId}`
);
}
idWritten = true;
// 3. Lock servo configuration (using NEW ID)
const [resLock, errLock] = await packetHandler.write1ByteTxRx(
portHandler,
newServoId, // Use NEW ID here
ADDR_SCS_LOCK,
1 // 1 to lock
);
if (resLock !== COMM_SUCCESS) {
// ID was likely changed, but lock failed. Critical state.
throw new Error(
`Failed to lock servo with new ID ${newServoId}: ${packetHandler.getTxRxResult(
resLock
)}, Error: ${errLock}. Configuration might be incomplete.`
);
}
unlocked = false; // Successfully locked with new ID
debugLog(
`Successfully set servo ID from ${currentServoId} to ${newServoId}. Remember to use the new ID for future commands.`
);
return "success";
} catch (err) {
console.error(`Exception during setServoId for current ID ${currentServoId}:`, err);
if (unlocked) {
// If unlock succeeded but subsequent steps failed, attempt to re-lock.
// If ID write failed, use current ID. If ID write succeeded but lock failed, use new ID.
const idToLock = idWritten ? newServoId : currentServoId;
console.warn(`Attempting to re-lock servo using ID ${idToLock}...`);
await tryLockServo(idToLock);
}
throw new Error(
`Failed to set servo ID from ${currentServoId} to ${newServoId}: ${err.message}`
);
}
}
// =============================================================================
// LEGACY COMPATIBILITY FUNCTIONS (for backward compatibility)
// =============================================================================
/**
* Sets a servo to wheel mode (continuous rotation) with unlocking.
* @param {number} servoId - The ID of the servo (1-252).
* @returns {Promise<"success">} Resolves with "success".
* @throws {Error} If not connected, any step fails, or an exception occurs.
*/
export async function setWheelMode(servoId) {
checkConnection();
let unlocked = false;
try {
debugLog(`Setting servo ${servoId} to wheel mode...`);
// 1. Unlock servo configuration
const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0);
if (resUnlock !== COMM_SUCCESS) {
throw new Error(`Failed to unlock servo ${servoId}: ${packetHandler.getTxRxResult(resUnlock)}, Error: ${errUnlock}`);
}
unlocked = true;
// 2. Set mode to 1 (Wheel/Speed mode)
const [resMode, errMode] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_MODE, 1);
if (resMode !== COMM_SUCCESS) {
throw new Error(`Failed to set wheel mode for servo ${servoId}: ${packetHandler.getTxRxResult(resMode)}, Error: ${errMode}`);
}
// 3. Lock servo configuration
const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1);
if (resLock !== COMM_SUCCESS) {
throw new Error(`Failed to lock servo ${servoId} after setting mode: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`);
}
unlocked = false;
debugLog(`Successfully set servo ${servoId} to wheel mode.`);
return "success";
} catch (err) {
console.error(`Exception setting wheel mode for servo ${servoId}:`, err);
if (unlocked) {
await tryLockServo(servoId);
}
throw new Error(`Failed to set wheel mode for servo ${servoId}: ${err.message}`);
}
}
/**
* Sets a servo back to position control mode from wheel mode.
* @param {number} servoId - The ID of the servo (1-252).
* @returns {Promise<"success">} Resolves with "success".
* @throws {Error} If not connected, any step fails, or an exception occurs.
*/
export async function setPositionMode(servoId) {
checkConnection();
let unlocked = false;
try {
debugLog(`Setting servo ${servoId} back to position mode...`);
// 1. Unlock servo configuration
const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0);
if (resUnlock !== COMM_SUCCESS) {
throw new Error(`Failed to unlock servo ${servoId}: ${packetHandler.getTxRxResult(resUnlock)}, Error: ${errUnlock}`);
}
unlocked = true;
// 2. Set mode to 0 (Position/Servo mode)
const [resMode, errMode] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_MODE, 0);
if (resMode !== COMM_SUCCESS) {
throw new Error(`Failed to set position mode for servo ${servoId}: ${packetHandler.getTxRxResult(resMode)}, Error: ${errMode}`);
}
// 3. Lock servo configuration
const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1);
if (resLock !== COMM_SUCCESS) {
throw new Error(`Failed to lock servo ${servoId} after setting mode: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`);
}
unlocked = false;
debugLog(`Successfully set servo ${servoId} back to position mode.`);
return "success";
} catch (err) {
console.error(`Exception setting position mode for servo ${servoId}:`, err);
if (unlocked) {
await tryLockServo(servoId);
}
throw new Error(`Failed to set position mode for servo ${servoId}: ${err.message}`);
}
}