Spaces:
Running
Running
// 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); | |
} | |
} | |