blanchon's picture
Update
f62f94b
// Constants
export const BROADCAST_ID = 0xFE; // 254
export const MAX_ID = 0xFC; // 252
// Protocol instructions
export const INST_PING = 1;
export const INST_READ = 2;
export const INST_WRITE = 3;
export const INST_REG_WRITE = 4;
export const INST_ACTION = 5;
export const INST_SYNC_WRITE = 131; // 0x83
export const INST_SYNC_READ = 130; // 0x82
export const INST_STATUS = 85; // 0x55, status packet instruction (0x55)
// Communication results
export const COMM_SUCCESS = 0; // tx or rx packet communication success
export const COMM_PORT_BUSY = -1; // Port is busy (in use)
export const COMM_TX_FAIL = -2; // Failed transmit instruction packet
export const COMM_RX_FAIL = -3; // Failed get status packet
export const COMM_TX_ERROR = -4; // Incorrect instruction packet
export const COMM_RX_WAITING = -5; // Now receiving status packet
export const COMM_RX_TIMEOUT = -6; // There is no status packet
export const COMM_RX_CORRUPT = -7; // Incorrect status packet
export const COMM_NOT_AVAILABLE = -9;
// Packet constants
export const TXPACKET_MAX_LEN = 250;
export const RXPACKET_MAX_LEN = 250;
// Protocol Packet positions
export const PKT_HEADER0 = 0;
export const PKT_HEADER1 = 1;
export const PKT_ID = 2;
export const PKT_LENGTH = 3;
export const PKT_INSTRUCTION = 4;
export const PKT_ERROR = 4;
export const PKT_PARAMETER0 = 5;
// Protocol Error bits
export const ERRBIT_VOLTAGE = 1;
export const ERRBIT_ANGLE = 2;
export const ERRBIT_OVERHEAT = 4;
export const ERRBIT_OVERELE = 8;
export const ERRBIT_OVERLOAD = 32;
// Default settings
const DEFAULT_BAUDRATE = 1000000;
const LATENCY_TIMER = 16;
// Global protocol end state
let SCS_END = 0; // (STS/SMS=0, SCS=1)
// Utility functions for handling word operations
export function SCS_LOWORD(l) {
return l & 0xFFFF;
}
export function SCS_HIWORD(l) {
return (l >> 16) & 0xFFFF;
}
export function SCS_LOBYTE(w) {
if (SCS_END === 0) {
return w & 0xFF;
} else {
return (w >> 8) & 0xFF;
}
}
export function SCS_HIBYTE(w) {
if (SCS_END === 0) {
return (w >> 8) & 0xFF;
} else {
return w & 0xFF;
}
}
export function SCS_MAKEWORD(a, b) {
if (SCS_END === 0) {
return (a & 0xFF) | ((b & 0xFF) << 8);
} else {
return (b & 0xFF) | ((a & 0xFF) << 8);
}
}
export function SCS_MAKEDWORD(a, b) {
return (a & 0xFFFF) | ((b & 0xFFFF) << 16);
}
export function SCS_TOHOST(a, b) {
if (a & (1 << b)) {
return -(a & ~(1 << b));
} else {
return a;
}
}
export class PortHandler {
constructor() {
this.port = null;
this.reader = null;
this.writer = null;
this.isOpen = false;
this.isUsing = false;
this.baudrate = DEFAULT_BAUDRATE;
this.packetStartTime = 0;
this.packetTimeout = 0;
this.txTimePerByte = 0;
}
async requestPort() {
try {
this.port = await navigator.serial.requestPort();
return true;
} catch (err) {
console.error('Error requesting serial port:', err);
return false;
}
}
async openPort() {
if (!this.port) {
return false;
}
try {
await this.port.open({ baudRate: this.baudrate });
this.reader = this.port.readable.getReader();
this.writer = this.port.writable.getWriter();
this.isOpen = true;
this.txTimePerByte = (1000.0 / this.baudrate) * 10.0;
return true;
} catch (err) {
console.error('Error opening port:', err);
return false;
}
}
async closePort() {
if (this.reader) {
await this.reader.releaseLock();
this.reader = null;
}
if (this.writer) {
await this.writer.releaseLock();
this.writer = null;
}
if (this.port && this.isOpen) {
await this.port.close();
this.isOpen = false;
}
}
async clearPort() {
if (this.reader) {
await this.reader.releaseLock();
this.reader = this.port.readable.getReader();
}
}
setBaudRate(baudrate) {
this.baudrate = baudrate;
this.txTimePerByte = (1000.0 / this.baudrate) * 10.0;
return true;
}
getBaudRate() {
return this.baudrate;
}
async writePort(data) {
if (!this.isOpen || !this.writer) {
return 0;
}
try {
await this.writer.write(new Uint8Array(data));
return data.length;
} catch (err) {
console.error('Error writing to port:', err);
return 0;
}
}
async readPort(length) {
if (!this.isOpen || !this.reader) {
return [];
}
try {
// Increase timeout for more reliable data reception
const timeoutMs = 500;
let totalBytes = [];
const startTime = performance.now();
// Continue reading until we get enough bytes or timeout
while (totalBytes.length < length) {
// Create a timeout promise
const timeoutPromise = new Promise(resolve => {
setTimeout(() => resolve({ value: new Uint8Array(), done: false, timeout: true }), 100); // Short internal timeout
});
// Race between reading and timeout
const result = await Promise.race([
this.reader.read(),
timeoutPromise
]);
if (result.timeout) {
// Internal timeout - check if we've exceeded total timeout
if (performance.now() - startTime > timeoutMs) {
console.log(`readPort total timeout after ${timeoutMs}ms`);
break;
}
continue; // Try reading again
}
if (result.done) {
console.log('Reader done, stream closed');
break;
}
if (result.value.length === 0) {
// If there's no data but we haven't timed out yet, wait briefly and try again
await new Promise(resolve => setTimeout(resolve, 10));
// Check if we've exceeded total timeout
if (performance.now() - startTime > timeoutMs) {
console.log(`readPort total timeout after ${timeoutMs}ms`);
break;
}
continue;
}
// Add received bytes to our total
const newData = Array.from(result.value);
totalBytes.push(...newData);
// console.log(`Read ${newData.length} bytes:`, newData.map(b => b.toString(16).padStart(2, '0')).join(' '));
// If we've got enough data, we can stop
if (totalBytes.length >= length) {
break;
}
}
return totalBytes;
} catch (err) {
console.error('Error reading from port:', err);
return [];
}
}
setPacketTimeout(packetLength) {
this.packetStartTime = this.getCurrentTime();
this.packetTimeout = (this.txTimePerByte * packetLength) + (LATENCY_TIMER * 2.0) + 2.0;
}
setPacketTimeoutMillis(msec) {
this.packetStartTime = this.getCurrentTime();
this.packetTimeout = msec;
}
isPacketTimeout() {
if (this.getTimeSinceStart() > this.packetTimeout) {
this.packetTimeout = 0;
return true;
}
return false;
}
getCurrentTime() {
return performance.now();
}
getTimeSinceStart() {
const timeSince = this.getCurrentTime() - this.packetStartTime;
if (timeSince < 0.0) {
this.packetStartTime = this.getCurrentTime();
}
return timeSince;
}
}
export class PacketHandler {
constructor(protocolEnd = 0) {
SCS_END = protocolEnd;
console.log(`PacketHandler initialized with protocol_end=${protocolEnd} (STS/SMS=0, SCS=1)`);
}
getProtocolVersion() {
return 1.0;
}
// 获取当前协议端设置的方法
getProtocolEnd() {
return SCS_END;
}
getTxRxResult(result) {
if (result === COMM_SUCCESS) {
return "[TxRxResult] Communication success!";
} else if (result === COMM_PORT_BUSY) {
return "[TxRxResult] Port is in use!";
} else if (result === COMM_TX_FAIL) {
return "[TxRxResult] Failed transmit instruction packet!";
} else if (result === COMM_RX_FAIL) {
return "[TxRxResult] Failed get status packet from device!";
} else if (result === COMM_TX_ERROR) {
return "[TxRxResult] Incorrect instruction packet!";
} else if (result === COMM_RX_WAITING) {
return "[TxRxResult] Now receiving status packet!";
} else if (result === COMM_RX_TIMEOUT) {
return "[TxRxResult] There is no status packet!";
} else if (result === COMM_RX_CORRUPT) {
return "[TxRxResult] Incorrect status packet!";
} else if (result === COMM_NOT_AVAILABLE) {
return "[TxRxResult] Protocol does not support this function!";
} else {
return "";
}
}
getRxPacketError(error) {
if (error & ERRBIT_VOLTAGE) {
return "[RxPacketError] Input voltage error!";
}
if (error & ERRBIT_ANGLE) {
return "[RxPacketError] Angle sen error!";
}
if (error & ERRBIT_OVERHEAT) {
return "[RxPacketError] Overheat error!";
}
if (error & ERRBIT_OVERELE) {
return "[RxPacketError] OverEle error!";
}
if (error & ERRBIT_OVERLOAD) {
return "[RxPacketError] Overload error!";
}
return "";
}
async txPacket(port, txpacket) {
let checksum = 0;
const totalPacketLength = txpacket[PKT_LENGTH] + 4; // 4: HEADER0 HEADER1 ID LENGTH
if (port.isUsing) {
return COMM_PORT_BUSY;
}
port.isUsing = true;
// Check max packet length
if (totalPacketLength > TXPACKET_MAX_LEN) {
port.isUsing = false;
return COMM_TX_ERROR;
}
// Make packet header
txpacket[PKT_HEADER0] = 0xFF;
txpacket[PKT_HEADER1] = 0xFF;
// Add checksum to packet
for (let idx = 2; idx < totalPacketLength - 1; idx++) {
checksum += txpacket[idx];
}
txpacket[totalPacketLength - 1] = (~checksum) & 0xFF;
// TX packet
await port.clearPort();
const writtenPacketLength = await port.writePort(txpacket);
if (totalPacketLength !== writtenPacketLength) {
port.isUsing = false;
return COMM_TX_FAIL;
}
return COMM_SUCCESS;
}
async rxPacket(port) {
let rxpacket = [];
let result = COMM_RX_FAIL;
let waitLength = 6; // minimum length (HEADER0 HEADER1 ID LENGTH)
while (true) {
const data = await port.readPort(waitLength - rxpacket.length);
rxpacket.push(...data);
if (rxpacket.length >= waitLength) {
// Find packet header
let headerIndex = -1;
for (let i = 0; i < rxpacket.length - 1; i++) {
if (rxpacket[i] === 0xFF && rxpacket[i + 1] === 0xFF) {
headerIndex = i;
break;
}
}
if (headerIndex === 0) {
// Found at the beginning of the packet
if (rxpacket[PKT_ID] > 0xFD || rxpacket[PKT_LENGTH] > RXPACKET_MAX_LEN) {
// Invalid ID or length
rxpacket.shift();
continue;
}
// Recalculate expected packet length
if (waitLength !== (rxpacket[PKT_LENGTH] + PKT_LENGTH + 1)) {
waitLength = rxpacket[PKT_LENGTH] + PKT_LENGTH + 1;
continue;
}
if (rxpacket.length < waitLength) {
// Check timeout
if (port.isPacketTimeout()) {
result = rxpacket.length === 0 ? COMM_RX_TIMEOUT : COMM_RX_CORRUPT;
break;
}
continue;
}
// Calculate checksum
let checksum = 0;
for (let i = 2; i < waitLength - 1; i++) {
checksum += rxpacket[i];
}
checksum = (~checksum) & 0xFF;
// Verify checksum
if (rxpacket[waitLength - 1] === checksum) {
result = COMM_SUCCESS;
} else {
result = COMM_RX_CORRUPT;
}
break;
} else if (headerIndex > 0) {
// Remove unnecessary bytes before header
rxpacket = rxpacket.slice(headerIndex);
continue;
}
}
// Check timeout
if (port.isPacketTimeout()) {
result = rxpacket.length === 0 ? COMM_RX_TIMEOUT : COMM_RX_CORRUPT;
break;
}
}
if (result !== COMM_SUCCESS) {
console.log(`rxPacket result: ${result}, packet: ${rxpacket.map(b => '0x' + b.toString(16).padStart(2,'0')).join(' ')}`);
} else {
// console.debug(`rxPacket successful: ${rxpacket.map(b => '0x' + b.toString(16).padStart(2,'0')).join(' ')}`);
}
return [rxpacket, result];
}
async txRxPacket(port, txpacket) {
let rxpacket = null;
let error = 0;
let result = COMM_TX_FAIL;
try {
// Check if port is already in use
if (port.isUsing) {
console.log("Port is busy, cannot start new transaction");
return [rxpacket, COMM_PORT_BUSY, error];
}
// TX packet
// console.log("Sending packet:", txpacket.map(b => '0x' + b.toString(16).padStart(2,'0')).join(' '));
// Remove retry logic and just send once
result = await this.txPacket(port, txpacket);
// console.log(`TX result: ${result}`);
if (result !== COMM_SUCCESS) {
console.log(`TX failed with result: ${result}`);
port.isUsing = false; // Important: Release the port on TX failure
return [rxpacket, result, error];
}
// If ID is broadcast, no need to wait for status packet
if (txpacket[PKT_ID] === BROADCAST_ID) {
port.isUsing = false;
return [rxpacket, result, error];
}
// Set packet timeout
if (txpacket[PKT_INSTRUCTION] === INST_READ) {
const length = txpacket[PKT_PARAMETER0 + 1];
// For READ instructions, we expect response to include the data
port.setPacketTimeout(length + 10); // Add extra buffer
// console.log(`Set READ packet timeout for ${length + 10} bytes`);
} else {
// For other instructions, we expect a status packet
port.setPacketTimeout(10); // HEADER0 HEADER1 ID LENGTH ERROR CHECKSUM + buffer
console.log(`Set standard packet timeout for 10 bytes`);
}
// RX packet - no retries, just attempt once
// console.log(`Receiving packet`);
// Clear port before receiving to ensure clean state
await port.clearPort();
const [rxpacketResult, resultRx] = await this.rxPacket(port);
rxpacket = rxpacketResult;
// Check if received packet is valid
if (resultRx !== COMM_SUCCESS) {
console.log(`Rx failed with result: ${resultRx}`);
port.isUsing = false;
return [rxpacket, resultRx, error];
}
// Verify packet structure
if (rxpacket.length < 6) {
console.log(`Received packet too short (${rxpacket.length} bytes)`);
port.isUsing = false;
return [rxpacket, COMM_RX_CORRUPT, error];
}
// Verify packet ID matches the sent ID
if (rxpacket[PKT_ID] !== txpacket[PKT_ID]) {
console.log(`Received packet ID (${rxpacket[PKT_ID]}) doesn't match sent ID (${txpacket[PKT_ID]})`);
port.isUsing = false;
return [rxpacket, COMM_RX_CORRUPT, error];
}
// Packet looks valid
error = rxpacket[PKT_ERROR];
port.isUsing = false; // Release port on success
return [rxpacket, resultRx, error];
} catch (err) {
console.error("Exception in txRxPacket:", err);
port.isUsing = false; // Release port on exception
return [rxpacket, COMM_RX_FAIL, error];
}
}
async ping(port, scsId) {
let modelNumber = 0;
let error = 0;
try {
if (scsId >= BROADCAST_ID) {
console.log(`Cannot ping broadcast ID ${scsId}`);
return [modelNumber, COMM_NOT_AVAILABLE, error];
}
const txpacket = new Array(6).fill(0);
txpacket[PKT_ID] = scsId;
txpacket[PKT_LENGTH] = 2;
txpacket[PKT_INSTRUCTION] = INST_PING;
console.log(`Pinging servo ID ${scsId}...`);
// 发送ping指令并获取响应
const [rxpacket, result, err] = await this.txRxPacket(port, txpacket);
error = err;
// 与Python SDK保持一致:如果ping成功,尝试读取地址3的型号信息
if (result === COMM_SUCCESS) {
console.log(`Ping to servo ID ${scsId} succeeded, reading model number from address 3`);
// 读取地址3的型号信息(2字节)
const [data, dataResult, dataError] = await this.readTxRx(port, scsId, 3, 2);
if (dataResult === COMM_SUCCESS && data && data.length >= 2) {
modelNumber = SCS_MAKEWORD(data[0], data[1]);
console.log(`Model number read: ${modelNumber}`);
} else {
console.log(`Could not read model number: ${this.getTxRxResult(dataResult)}`);
}
} else {
console.log(`Ping failed with result: ${result}, error: ${error}`);
}
return [modelNumber, result, error];
} catch (error) {
console.error(`Exception in ping():`, error);
return [0, COMM_RX_FAIL, 0];
}
}
// Read methods
async readTxRx(port, scsId, address, length) {
if (scsId >= BROADCAST_ID) {
console.log('Cannot read from broadcast ID');
return [[], COMM_NOT_AVAILABLE, 0];
}
// Create read packet
const txpacket = new Array(8).fill(0);
txpacket[PKT_ID] = scsId;
txpacket[PKT_LENGTH] = 4;
txpacket[PKT_INSTRUCTION] = INST_READ;
txpacket[PKT_PARAMETER0] = address;
txpacket[PKT_PARAMETER0 + 1] = length;
// console.log(`Reading ${length} bytes from address ${address} for servo ID ${scsId}`);
// Send packet and get response
const [rxpacket, result, error] = await this.txRxPacket(port, txpacket);
// Process the result
if (result !== COMM_SUCCESS) {
console.log(`Read failed with result: ${result}, error: ${error}`);
return [[], result, error];
}
if (!rxpacket || rxpacket.length < PKT_PARAMETER0 + length) {
console.log(`Invalid response packet: expected at least ${PKT_PARAMETER0 + length} bytes, got ${rxpacket ? rxpacket.length : 0}`);
return [[], COMM_RX_CORRUPT, error];
}
// Extract data from response
const data = [];
// console.log(`Response packet length: ${rxpacket.length}, extracting ${length} bytes from offset ${PKT_PARAMETER0}`);
// console.log(`Response data bytes: ${rxpacket.slice(PKT_PARAMETER0, PKT_PARAMETER0 + length).map(b => '0x' + b.toString(16).padStart(2,'0')).join(' ')}`);
for (let i = 0; i < length; i++) {
data.push(rxpacket[PKT_PARAMETER0 + i]);
}
// console.log(`Successfully read ${length} bytes: ${data.map(b => '0x' + b.toString(16).padStart(2,'0')).join(' ')}`);
return [data, result, error];
}
async read1ByteTxRx(port, scsId, address) {
const [data, result, error] = await this.readTxRx(port, scsId, address, 1);
const value = (data.length > 0) ? data[0] : 0;
return [value, result, error];
}
async read2ByteTxRx(port, scsId, address) {
const [data, result, error] = await this.readTxRx(port, scsId, address, 2);
let value = 0;
if (data.length >= 2) {
value = SCS_MAKEWORD(data[0], data[1]);
}
return [value, result, error];
}
async read4ByteTxRx(port, scsId, address) {
const [data, result, error] = await this.readTxRx(port, scsId, address, 4);
let value = 0;
if (data.length >= 4) {
const loword = SCS_MAKEWORD(data[0], data[1]);
const hiword = SCS_MAKEWORD(data[2], data[3]);
value = SCS_MAKEDWORD(loword, hiword);
console.log(`read4ByteTxRx: data=${data.map(b => '0x' + b.toString(16).padStart(2,'0')).join(' ')}`);
console.log(` loword=${loword} (0x${loword.toString(16)}), hiword=${hiword} (0x${hiword.toString(16)})`);
console.log(` value=${value} (0x${value.toString(16)})`);
}
return [value, result, error];
}
// Write methods
async writeTxRx(port, scsId, address, length, data) {
if (scsId >= BROADCAST_ID) {
return [COMM_NOT_AVAILABLE, 0];
}
// Create write packet
const txpacket = new Array(length + 7).fill(0);
txpacket[PKT_ID] = scsId;
txpacket[PKT_LENGTH] = length + 3;
txpacket[PKT_INSTRUCTION] = INST_WRITE;
txpacket[PKT_PARAMETER0] = address;
// Add data
for (let i = 0; i < length; i++) {
txpacket[PKT_PARAMETER0 + 1 + i] = data[i] & 0xFF;
}
// Send packet and get response
const [rxpacket, result, error] = await this.txRxPacket(port, txpacket);
return [result, error];
}
async write1ByteTxRx(port, scsId, address, data) {
const dataArray = [data & 0xFF];
return await this.writeTxRx(port, scsId, address, 1, dataArray);
}
async write2ByteTxRx(port, scsId, address, data) {
const dataArray = [
SCS_LOBYTE(data),
SCS_HIBYTE(data)
];
return await this.writeTxRx(port, scsId, address, 2, dataArray);
}
async write4ByteTxRx(port, scsId, address, data) {
const dataArray = [
SCS_LOBYTE(SCS_LOWORD(data)),
SCS_HIBYTE(SCS_LOWORD(data)),
SCS_LOBYTE(SCS_HIWORD(data)),
SCS_HIBYTE(SCS_HIWORD(data))
];
return await this.writeTxRx(port, scsId, address, 4, dataArray);
}
// Add syncReadTx for GroupSyncRead functionality
async syncReadTx(port, startAddress, dataLength, param, paramLength) {
// Create packet: HEADER0 HEADER1 ID LEN INST START_ADDR DATA_LEN PARAM... CHKSUM
const txpacket = new Array(paramLength + 8).fill(0);
txpacket[PKT_ID] = BROADCAST_ID;
txpacket[PKT_LENGTH] = paramLength + 4; // 4: INST START_ADDR DATA_LEN CHKSUM
txpacket[PKT_INSTRUCTION] = INST_SYNC_READ;
txpacket[PKT_PARAMETER0] = startAddress;
txpacket[PKT_PARAMETER0 + 1] = dataLength;
// Add parameters
for (let i = 0; i < paramLength; i++) {
txpacket[PKT_PARAMETER0 + 2 + i] = param[i];
}
// Calculate checksum
const totalLen = txpacket[PKT_LENGTH] + 4; // 4: HEADER0 HEADER1 ID LENGTH
// Add headers
txpacket[PKT_HEADER0] = 0xFF;
txpacket[PKT_HEADER1] = 0xFF;
// Calculate checksum
let checksum = 0;
for (let i = 2; i < totalLen - 1; i++) {
checksum += txpacket[i] & 0xFF;
}
txpacket[totalLen - 1] = (~checksum) & 0xFF;
console.log(`SyncReadTx: ${txpacket.map(b => '0x' + b.toString(16).padStart(2,'0')).join(' ')}`);
// Send packet
await port.clearPort();
const bytesWritten = await port.writePort(txpacket);
if (bytesWritten !== totalLen) {
return COMM_TX_FAIL;
}
// Set timeout based on expected response size
port.setPacketTimeout((6 + dataLength) * paramLength);
return COMM_SUCCESS;
}
// Add syncWriteTxOnly for GroupSyncWrite functionality
async syncWriteTxOnly(port, startAddress, dataLength, param, paramLength) {
// Create packet: HEADER0 HEADER1 ID LEN INST START_ADDR DATA_LEN PARAM... CHKSUM
const txpacket = new Array(paramLength + 8).fill(0);
txpacket[PKT_ID] = BROADCAST_ID;
txpacket[PKT_LENGTH] = paramLength + 4; // 4: INST START_ADDR DATA_LEN CHKSUM
txpacket[PKT_INSTRUCTION] = INST_SYNC_WRITE;
txpacket[PKT_PARAMETER0] = startAddress;
txpacket[PKT_PARAMETER0 + 1] = dataLength;
// Add parameters
for (let i = 0; i < paramLength; i++) {
txpacket[PKT_PARAMETER0 + 2 + i] = param[i];
}
// Calculate checksum
const totalLen = txpacket[PKT_LENGTH] + 4; // 4: HEADER0 HEADER1 ID LENGTH
// Add headers
txpacket[PKT_HEADER0] = 0xFF;
txpacket[PKT_HEADER1] = 0xFF;
// Calculate checksum
let checksum = 0;
for (let i = 2; i < totalLen - 1; i++) {
checksum += txpacket[i] & 0xFF;
}
txpacket[totalLen - 1] = (~checksum) & 0xFF;
// console.log(`SyncWriteTxOnly: ${txpacket.map(b => '0x' + b.toString(16).padStart(2,'0')).join(' ')}`);
// Send packet - for sync write, we don't need a response
await port.clearPort();
const bytesWritten = await port.writePort(txpacket);
if (bytesWritten !== totalLen) {
return COMM_TX_FAIL;
}
return COMM_SUCCESS;
}
// 辅助方法:格式化数据包结构以方便调试
formatPacketStructure(packet) {
if (!packet || packet.length < 4) {
return "Invalid packet (too short)";
}
try {
let result = "";
result += `HEADER: ${packet[0].toString(16).padStart(2,'0')} ${packet[1].toString(16).padStart(2,'0')} | `;
result += `ID: ${packet[2]} | `;
result += `LENGTH: ${packet[3]} | `;
if (packet.length >= 5) {
result += `ERROR/INST: ${packet[4].toString(16).padStart(2,'0')} | `;
}
if (packet.length >= 6) {
result += "PARAMS: ";
for (let i = 5; i < packet.length - 1; i++) {
result += `${packet[i].toString(16).padStart(2,'0')} `;
}
result += `| CHECKSUM: ${packet[packet.length-1].toString(16).padStart(2,'0')}`;
}
return result;
} catch (e) {
return "Error formatting packet: " + e.message;
}
}
/**
* 从响应包中解析舵机型号
* @param {Array} rxpacket - 响应数据包
* @returns {number} 舵机型号
*/
parseModelNumber(rxpacket) {
if (!rxpacket || rxpacket.length < 7) {
return 0;
}
// 检查是否有参数字段
if (rxpacket.length <= PKT_PARAMETER0 + 1) {
return 0;
}
const param1 = rxpacket[PKT_PARAMETER0];
const param2 = rxpacket[PKT_PARAMETER0 + 1];
if (SCS_END === 0) {
// STS/SMS 协议的字节顺序
return SCS_MAKEWORD(param1, param2);
} else {
// SCS 协议的字节顺序
return SCS_MAKEWORD(param2, param1);
}
}
/**
* Verify packet header
* @param {Array} packet - The packet to verify
* @returns {Number} COMM_SUCCESS if packet is valid, error code otherwise
*/
getPacketHeader(packet) {
if (!packet || packet.length < 4) {
return COMM_RX_CORRUPT;
}
// Check header
if (packet[PKT_HEADER0] !== 0xFF || packet[PKT_HEADER1] !== 0xFF) {
return COMM_RX_CORRUPT;
}
// Check ID validity
if (packet[PKT_ID] > 0xFD) {
return COMM_RX_CORRUPT;
}
// Check length
if (packet.length != (packet[PKT_LENGTH] + 4)) {
return COMM_RX_CORRUPT;
}
// Calculate checksum
let checksum = 0;
for (let i = 2; i < packet.length - 1; i++) {
checksum += packet[i] & 0xFF;
}
checksum = (~checksum) & 0xFF;
// Verify checksum
if (packet[packet.length - 1] !== checksum) {
return COMM_RX_CORRUPT;
}
return COMM_SUCCESS;
}
}
/**
* GroupSyncRead class
* - This class is used to read multiple servos with the same control table address at once
*/
export class GroupSyncRead {
constructor(port, ph, startAddress, dataLength) {
this.port = port;
this.ph = ph;
this.startAddress = startAddress;
this.dataLength = dataLength;
this.isAvailableServiceID = new Set();
this.dataDict = new Map();
this.param = [];
this.clearParam();
}
makeParam() {
this.param = [];
for (const id of this.isAvailableServiceID) {
this.param.push(id);
}
return this.param.length;
}
addParam(scsId) {
if (this.isAvailableServiceID.has(scsId)) {
return false;
}
this.isAvailableServiceID.add(scsId);
this.dataDict.set(scsId, new Array(this.dataLength).fill(0));
return true;
}
removeParam(scsId) {
if (!this.isAvailableServiceID.has(scsId)) {
return false;
}
this.isAvailableServiceID.delete(scsId);
this.dataDict.delete(scsId);
return true;
}
clearParam() {
this.isAvailableServiceID.clear();
this.dataDict.clear();
return true;
}
async txPacket() {
if (this.isAvailableServiceID.size === 0) {
return COMM_NOT_AVAILABLE;
}
const paramLength = this.makeParam();
return await this.ph.syncReadTx(this.port, this.startAddress, this.dataLength, this.param, paramLength);
}
async rxPacket() {
let result = COMM_RX_FAIL;
if (this.isAvailableServiceID.size === 0) {
return COMM_NOT_AVAILABLE;
}
// Set all servos' data as invalid
for (const id of this.isAvailableServiceID) {
this.dataDict.set(id, new Array(this.dataLength).fill(0));
console.log(`Cleared data for servo ID ${id}`);
}
const [rxpacket, rxResult] = await this.ph.rxPacket(this.port);
if (rxResult !== COMM_SUCCESS || !rxpacket || rxpacket.length === 0) {
return rxResult;
}
// More tolerant of packets with unexpected values in the PKT_ERROR field
// Don't require INST_STATUS to be exactly 0x55
console.log(`GroupSyncRead rxPacket: ID=${rxpacket[PKT_ID]}, ERROR/INST field=0x${rxpacket[PKT_ERROR].toString(16)}`);
// Check if the packet matches any of the available IDs
if (!this.isAvailableServiceID.has(rxpacket[PKT_ID])) {
console.log(`Received packet with ID ${rxpacket[PKT_ID]} which is not in the available IDs list`);
return COMM_RX_CORRUPT;
}
// Extract data for the matching ID
const scsId = rxpacket[PKT_ID];
const data = new Array(this.dataLength).fill(0);
// Extract the parameter data, which should start at PKT_PARAMETER0
if (rxpacket.length < PKT_PARAMETER0 + this.dataLength) {
console.log(`Packet too short: expected ${PKT_PARAMETER0 + this.dataLength} bytes, got ${rxpacket.length}`);
return COMM_RX_CORRUPT;
}
for (let i = 0; i < this.dataLength; i++) {
data[i] = rxpacket[PKT_PARAMETER0 + i];
}
// Update the data dict
this.dataDict.set(scsId, data);
console.log(`Updated data for servo ID ${scsId}: ${data.map(b => '0x' + b.toString(16).padStart(2,'0')).join(' ')}`);
// Continue receiving until timeout or all data is received
if (this.isAvailableServiceID.size > 1) {
result = await this.rxPacket();
} else {
result = COMM_SUCCESS;
}
return result;
}
async txRxPacket() {
try {
// First check if port is being used
if (this.port.isUsing) {
console.log("Port is busy, cannot start sync read operation");
return COMM_PORT_BUSY;
}
// Start the transmission
console.log("Starting sync read TX/RX operation...");
let result = await this.txPacket();
if (result !== COMM_SUCCESS) {
console.log(`Sync read TX failed with result: ${result}`);
return result;
}
// Get a single response with a standard timeout
console.log(`Attempting to receive a response...`);
// Receive a single response
result = await this.rxPacket();
console.log(`Sync read RX result###: ${result}`);
// Release port
this.port.isUsing = false;
return result;
} catch (error) {
console.error("Exception in GroupSyncRead txRxPacket:", error);
// Make sure port is released
this.port.isUsing = false;
return COMM_RX_FAIL;
}
}
isAvailable(scsId, address, dataLength) {
if (!this.isAvailableServiceID.has(scsId)) {
return false;
}
const startAddr = this.startAddress;
const endAddr = startAddr + this.dataLength - 1;
const reqStartAddr = address;
const reqEndAddr = reqStartAddr + dataLength - 1;
if (reqStartAddr < startAddr || reqEndAddr > endAddr) {
return false;
}
const data = this.dataDict.get(scsId);
if (!data || data.length === 0) {
return false;
}
return true;
}
getData(scsId, address, dataLength) {
if (!this.isAvailable(scsId, address, dataLength)) {
return 0;
}
const startAddr = this.startAddress;
const data = this.dataDict.get(scsId);
// Calculate data offset
const dataOffset = address - startAddr;
// Combine bytes according to dataLength
switch (dataLength) {
case 1:
return data[dataOffset];
case 2:
return SCS_MAKEWORD(data[dataOffset], data[dataOffset + 1]);
case 4:
return SCS_MAKEDWORD(
SCS_MAKEWORD(data[dataOffset], data[dataOffset + 1]),
SCS_MAKEWORD(data[dataOffset + 2], data[dataOffset + 3])
);
default:
return 0;
}
}
}
/**
* GroupSyncWrite class
* - This class is used to write multiple servos with the same control table address at once
*/
export class GroupSyncWrite {
constructor(port, ph, startAddress, dataLength) {
this.port = port;
this.ph = ph;
this.startAddress = startAddress;
this.dataLength = dataLength;
this.isAvailableServiceID = new Set();
this.dataDict = new Map();
this.param = [];
this.clearParam();
}
makeParam() {
this.param = [];
for (const id of this.isAvailableServiceID) {
// Add ID to parameter
this.param.push(id);
// Add data to parameter
const data = this.dataDict.get(id);
for (let i = 0; i < this.dataLength; i++) {
this.param.push(data[i]);
}
}
return this.param.length;
}
addParam(scsId, data) {
if (this.isAvailableServiceID.has(scsId)) {
return false;
}
if (data.length !== this.dataLength) {
console.error(`Data length (${data.length}) doesn't match required length (${this.dataLength})`);
return false;
}
this.isAvailableServiceID.add(scsId);
this.dataDict.set(scsId, data);
return true;
}
removeParam(scsId) {
if (!this.isAvailableServiceID.has(scsId)) {
return false;
}
this.isAvailableServiceID.delete(scsId);
this.dataDict.delete(scsId);
return true;
}
changeParam(scsId, data) {
if (!this.isAvailableServiceID.has(scsId)) {
return false;
}
if (data.length !== this.dataLength) {
console.error(`Data length (${data.length}) doesn't match required length (${this.dataLength})`);
return false;
}
this.dataDict.set(scsId, data);
return true;
}
clearParam() {
this.isAvailableServiceID.clear();
this.dataDict.clear();
return true;
}
async txPacket() {
if (this.isAvailableServiceID.size === 0) {
return COMM_NOT_AVAILABLE;
}
const paramLength = this.makeParam();
return await this.ph.syncWriteTxOnly(this.port, this.startAddress, this.dataLength, this.param, paramLength);
}
}