Spaces:
Running
Running
/** | |
* Read-Only Servo SDK for USB Master | |
* Simplified version that only reads positions without any locking | |
*/ | |
import { PortHandler, PacketHandler, COMM_SUCCESS, GroupSyncRead } from "./lowLevelSDK.mjs"; | |
import { ADDR_SCS_PRESENT_POSITION } from "./scsservo_constants.mjs"; | |
// Module-level variables for handlers | |
let portHandler = null; | |
let packetHandler = null; | |
/** | |
* 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) { | |
console.log("π Already connected to USB robot (read-only mode)."); | |
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); | |
console.log( | |
`π Connected to USB robot (read-only mode) at ${baudRate} baud, protocol end: ${protocolEnd}.` | |
); | |
return true; | |
} catch (err) { | |
console.error("Error during USB robot 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(`USB robot 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) { | |
console.log("Already disconnected from USB robot."); | |
return true; | |
} | |
try { | |
await portHandler.closePort(); | |
portHandler = null; | |
packetHandler = null; | |
console.log("π Disconnected from USB robot (read-only mode)."); | |
return true; | |
} catch (err) { | |
console.error("Error during USB robot disconnection:", err); | |
portHandler = null; | |
packetHandler = null; | |
throw new Error(`USB robot disconnection failed: ${err.message}`); | |
} | |
} | |
/** | |
* 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 USB robot. Call connect() first."); | |
} | |
} | |
/** | |
* 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 position of multiple servos synchronously. | |
* Returns positions for all servos that respond, skipping failed ones gracefully. | |
* @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) { | |
console.log("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) { | |
console.log("Sync Read: No valid servo IDs to read."); | |
return new Map(); | |
} | |
try { | |
// Send the Sync Read instruction packet | |
let txResult = await groupSyncRead.txPacket(); | |
if (txResult !== COMM_SUCCESS) { | |
throw new Error(`Sync Read txPacket failed: ${packetHandler.getTxRxResult(txResult)}`); | |
} | |
// Receive the response packets | |
let rxResult = await groupSyncRead.rxPacket(); | |
if (rxResult !== COMM_SUCCESS) { | |
console.warn( | |
`Sync Read rxPacket overall result: ${packetHandler.getTxRxResult( | |
rxResult | |
)}. Checking individual servos.` | |
); | |
} | |
// Check data availability and retrieve data for each servo | |
const failedIds = []; | |
validIds.forEach((id) => { | |
const isAvailable = groupSyncRead.isAvailable(id, startAddress, dataLength); | |
if (isAvailable) { | |
const position = groupSyncRead.getData(id, startAddress, dataLength); | |
const finalPosition = position & 0xffff; | |
console.log( | |
`π Debug Servo ${id}: raw=${position}, final=${finalPosition}, hex=${position.toString(16)}` | |
); | |
positions.set(id, finalPosition); | |
} else { | |
failedIds.push(id); | |
} | |
}); | |
// Log failed servos but don't throw error - return available data | |
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}`); | |
} | |
} | |