Program Listing for File perseus-arm-teleop.cpp¶

↰ Return to documentation for file (software/arm-teleop-direct/src/perseus-arm-teleop.cpp)

// For controlling one pair of SO-100 arms

#include "perseus-arm-teleop.hpp"

#include <array>
#include <chrono>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <thread>

#include "servo-constants.hpp"

using namespace boost::asio;
using namespace perseus::servo;

ST3215ServoReader::ST3215ServoReader(const std::string& port, unsigned int baud_rate, uint8_t acceleration)
    : _io_service(),
      _serial_port(_io_service)
{
    try
    {
        _serial_port.open(port);

        // Get the native handle for low-level configuration
        int fd = _serial_port.native_handle();

        // Configure ACM port settings
        struct termios to;
        if (tcgetattr(fd, &to) != 0)
        {
            throw std::runtime_error("Failed to get port attributes");
        }

        // Input flags - disable break processing
        to.c_iflag &= ~(IGNBRK | BRKINT | ICRNL | INLCR | PARMRK | INPCK | ISTRIP | IXON);

        // Output flags - disable post processing
        to.c_oflag &= ~(OCRNL | ONLCR | ONLRET | ONOCR | OFILL | OPOST);

        // No line processing
        to.c_lflag &= ~(ECHO | ECHONL | ICANON | IEXTEN | ISIG);

        // Clean character processing
        to.c_cflag &= ~(CSIZE | PARENB | PARODD | CSTOPB);
        to.c_cflag |= CS8 | CLOCAL | CREAD;

        // Set speed
        if (cfsetspeed(&to, baud_rate) != 0)
        {
            throw std::runtime_error("Failed to set baud rate");
        }

        // One byte at a time, no timer
        to.c_cc[VMIN] = 1;
        to.c_cc[VTIME] = 0;

        // Apply settings
        if (tcsetattr(fd, TCSANOW, &to) != 0)
        {
            throw std::runtime_error("Failed to apply port attributes");
        }

        // Clear all IO buffers
        if (tcflush(fd, TCIOFLUSH) != 0)
        {
            throw std::runtime_error("Failed to flush port");
        }

        // We still set the boost::asio options for consistency
        _serial_port.set_option(serial_port::baud_rate(baud_rate));
        _serial_port.set_option(serial_port::character_size(8));
        _serial_port.set_option(serial_port::stop_bits(serial_port::stop_bits::one));
        _serial_port.set_option(serial_port::parity(serial_port::parity::none));
        _serial_port.set_option(serial_port::flow_control(serial_port::flow_control::none));

        // Initial delay to let port settle - ACM devices often need more time
        std::this_thread::sleep_for(timing::PORT_SETTLE_TIME);

        // Set acceleration for all servos (1-6)
        for (uint8_t id = 1; id <= 6; ++id)
        {
            try
            {
                write_control_register(id, register_addr::ACCELERATION, acceleration);
                std::this_thread::sleep_for(timing::MIN_RESPONSE_TIME);  // Wait between writes
            }
            catch (const std::exception& e)
            {
                std::cerr << "Warning: Failed to set acceleration for servo " << static_cast<int>(id)
                          << ": " << e.what() << std::endl;
            }
        }
    }
    catch (const boost::system::system_error& e)
    {
        throw std::runtime_error(std::string("Failed to open serial port: ") + e.what());
    }
}

ST3215ServoReader::~ST3215ServoReader()
{
    try
    {
        if (_serial_port.is_open())
        {
            ::tcflush(static_cast<int>(_serial_port.native_handle()), TCIOFLUSH);
            _serial_port.close();
        }
    }
    catch (...)
    {
        // Ignore errors in destructor
    }
}

uint16_t ST3215ServoReader::read_position(uint8_t servo_id)
{
    for (int retry = 0; retry < communication::MAX_RETRIES; ++retry)
    {
        try
        {
            return _read_position_once(servo_id, timing::DEFAULT_TIMEOUT);
        }
        catch (const std::runtime_error& e)
        {
            if (retry == communication::MAX_RETRIES - 1)
            {
                throw;  // Re-throw if this was our last retry
            }
            // Flush the port and wait before retry
            ::tcflush(static_cast<int>(_serial_port.native_handle()), TCIOFLUSH);
            std::this_thread::sleep_for(timing::RETRY_DELAY);
        }
    }
    throw std::runtime_error("Maximum retries exceeded");
}

#include <fcntl.h>
#include <termios.h>

uint16_t ST3215ServoReader::_read_position_once(uint8_t servo_id, const std::chrono::milliseconds& timeout)
{
    // Create read position command packet
    std::vector<uint8_t> command = _create_read_command(servo_id, register_addr::PRESENT_POSITION, 2);

    // Clear any existing data and wait for port to clear
    ::tcflush(static_cast<int>(_serial_port.native_handle()), TCIOFLUSH);
    std::this_thread::sleep_for(timing::COMMAND_INTERVAL);

    // Send command with retry
    boost::system::error_code write_ec;
    size_t written = 0;
    int write_attempts = 0;
    const int MAX_WRITE_ATTEMPTS = 3;

    while (written != command.size() && write_attempts < MAX_WRITE_ATTEMPTS)
    {
        written = boost::asio::write(_serial_port, buffer(command), write_ec);
        if (write_ec || written != command.size())
        {
            write_attempts++;
            if (write_attempts < MAX_WRITE_ATTEMPTS)
            {
                std::this_thread::sleep_for(timing::MIN_RESPONSE_TIME);
                continue;
            }
        }
        break;
    }
    if (write_ec)
    {
        throw std::runtime_error(std::string("Write error: ") + write_ec.message());
    }
    if (written != command.size())
    {
        throw std::runtime_error("Failed to write complete command");
    }

    // Ensure minimum response time
    std::this_thread::sleep_for(timing::MIN_RESPONSE_TIME);

    // Read response using a fixed buffer
    std::array<uint8_t, 256> response_buffer;
    size_t total_read = 0;
    const size_t HEADER_SIZE = 4;
    auto start_time = std::chrono::steady_clock::now();

    // Read header with timeout
    while (total_read < HEADER_SIZE)
    {
        if (std::chrono::steady_clock::now() - start_time > timeout)
        {
            throw std::runtime_error("Timeout waiting for header");
        }

        boost::system::error_code read_ec;
        size_t bytes = _serial_port.read_some(
            buffer(response_buffer.data() + total_read, HEADER_SIZE - total_read),
            read_ec);

        if (read_ec)
        {
            if (read_ec == boost::asio::error::operation_aborted ||
                read_ec == boost::asio::error::interrupted)
            {
                continue;  // Retry on interruption
            }
            throw std::runtime_error(std::string("Header read error: ") + read_ec.message());
        }

        if (bytes > 0)
        {
            total_read += bytes;
        }
        else
        {
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
        }
    }

    // Validate header
    if (response_buffer[0] != protocol::HEADER1 || response_buffer[1] != protocol::HEADER2)
    {
        throw std::runtime_error("Invalid header markers");
    }
    if (response_buffer[2] != servo_id)
    {
        throw std::runtime_error("Mismatched servo ID");
    }
    if (response_buffer[3] < 4)
    {
        throw std::runtime_error("Invalid length");
    }

    // Read remaining data
    const size_t remaining_bytes = response_buffer[3];
    total_read = 0;
    start_time = std::chrono::steady_clock::now();

    while (total_read < remaining_bytes)
    {
        if (std::chrono::steady_clock::now() - start_time > timeout)
        {
            throw std::runtime_error("Timeout waiting for data");
        }

        boost::system::error_code read_ec;
        size_t bytes = _serial_port.read_some(
            buffer(response_buffer.data() + HEADER_SIZE + total_read,
                   remaining_bytes - total_read),
            read_ec);

        if (read_ec)
        {
            if (read_ec == boost::asio::error::operation_aborted ||
                read_ec == boost::asio::error::interrupted)
            {
                continue;  // Retry on interruption
            }
            throw std::runtime_error(std::string("Data read error: ") + read_ec.message());
        }

        if (bytes > 0)
        {
            total_read += bytes;
        }
        else
        {
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
        }
    }

    // Check for servo errors
    if (response_buffer[HEADER_SIZE] != 0x00)
    {
        std::string error = "Servo errors:";
        if (response_buffer[HEADER_SIZE] & error_bits::INPUT_VOLTAGE)
            error += " Input Voltage";
        if (response_buffer[HEADER_SIZE] & error_bits::ANGLE_LIMIT)
            error += " Angle Limit";
        if (response_buffer[HEADER_SIZE] & error_bits::OVERHEATING)
            error += " Overheating";
        if (response_buffer[HEADER_SIZE] & error_bits::RANGE)
            error += " Range";
        if (response_buffer[HEADER_SIZE] & error_bits::CHECKSUM)
            error += " Checksum";
        if (response_buffer[HEADER_SIZE] & error_bits::OVERLOAD)
            error += " Overload";
        if (response_buffer[HEADER_SIZE] & error_bits::INSTRUCTION)
            error += " Instruction";
        throw std::runtime_error(error);
    }

    // Position is in little-endian format
    return static_cast<uint16_t>(response_buffer[HEADER_SIZE + 1]) |
           (static_cast<uint16_t>(response_buffer[HEADER_SIZE + 2]) << 8);
}

std::vector<uint8_t> ST3215ServoReader::_create_read_command(uint8_t id, uint8_t address, uint8_t size)
{
    std::vector<uint8_t> command = {
        protocol::HEADER1, protocol::HEADER2,  // Header
        id,                                    // Servo ID
        0x04,                                  // Length (always 4 for read command)
        instruction::READ,                     // READ instruction
        address,                               // Starting address
        size,                                  // Number of bytes to read
        0x00                                   // Checksum (to be calculated)
    };

    // Calculate checksum
    uint8_t checksum = 0;
    for (size_t i = 2; i < command.size() - 1; i++)
    {
        checksum += command[i];
    }
    command[command.size() - 1] = ~checksum;

    return command;
}

std::vector<uint8_t> ST3215ServoReader::_create_write_command(uint8_t id, uint8_t address, const std::vector<uint8_t>& data)
{
    std::vector<uint8_t> command;
    command.reserve(data.size() + 6);  // Pre-allocate space

    // Header
    command.push_back(protocol::HEADER1);
    command.push_back(protocol::HEADER2);
    command.push_back(id);
    command.push_back(data.size() + 3);     // Length (data size + 3 bytes for instruction, address, and checksum)
    command.push_back(instruction::WRITE);  // WRITE instruction
    command.push_back(address);

    // Data
    command.insert(command.end(), data.begin(), data.end());

    // Calculate checksum
    uint8_t checksum = 0;
    for (size_t i = 2; i < command.size(); i++)
    {
        checksum += command[i];
    }
    command.push_back(~checksum);

    return command;
}

void ST3215ServoReader::write_position(uint8_t servo_id, uint16_t position)
{
    // Clamp position to valid range
    position = std::min(position, limits::MAX_POSITION);

    // Create data bytes (little-endian)
    std::vector<uint8_t> data = {
        static_cast<uint8_t>(position & 0xFF),
        static_cast<uint8_t>((position >> 8) & 0xFF)};

    // Create write command packet
    std::vector<uint8_t> command = _create_write_command(servo_id, register_addr::GOAL_POSITION, data);

    // Send command
    boost::system::error_code write_ec;
    size_t written = boost::asio::write(_serial_port, buffer(command), write_ec);

    if (write_ec)
    {
        throw std::runtime_error(std::string("Write error: ") + write_ec.message());
    }

    if (written != command.size())
    {
        throw std::runtime_error("Failed to write complete command");
    }

    // Wait for minimum response time
    std::this_thread::sleep_for(timing::MIN_RESPONSE_TIME);

    // Read status packet
    std::array<uint8_t, 6> response;  // Status packet is 6 bytes
    size_t read = 0;

    try
    {
        read = boost::asio::read(_serial_port, buffer(response), write_ec);
    }
    catch (const boost::system::system_error& e)
    {
        // Ignore read errors as the servo might not respond
        return;
    }

    if (read >= 4 && response[0] == protocol::HEADER1 && response[1] == protocol::HEADER2 &&
        response[2] == servo_id && response[3] >= 2)
    {
        // Check for servo errors if we got a valid response
        if (response[4] != 0x00)
        {
            std::string error = "Servo errors:";
            if (response[4] & error_bits::INPUT_VOLTAGE)
                error += " Input Voltage";
            if (response[4] & error_bits::ANGLE_LIMIT)
                error += " Angle Limit";
            if (response[4] & error_bits::OVERHEATING)
                error += " Overheating";
            if (response[4] & error_bits::RANGE)
                error += " Range";
            if (response[4] & error_bits::CHECKSUM)
                error += " Checksum";
            if (response[4] & error_bits::OVERLOAD)
                error += " Overload";
            if (response[4] & error_bits::INSTRUCTION)
                error += " Instruction";
            throw std::runtime_error(error);
        }
    }
}

void ST3215ServoReader::write_control_register(uint8_t servo_id, uint8_t address, uint8_t value)
{
    std::vector<uint8_t> data = {value};
    std::vector<uint8_t> command = _create_write_command(servo_id, address, data);

    boost::system::error_code write_ec;
    size_t written = boost::asio::write(_serial_port, buffer(command), write_ec);

    if (write_ec)
    {
        throw std::runtime_error(std::string("Write error: ") + write_ec.message());
    }

    if (written != command.size())
    {
        throw std::runtime_error("Failed to write complete command");
    }

    // Wait for minimum response time
    std::this_thread::sleep_for(timing::MIN_RESPONSE_TIME);

    // Read status packet
    std::array<uint8_t, 6> response;
    try
    {
        boost::asio::read(_serial_port, buffer(response), write_ec);
    }
    catch (const boost::system::system_error& e)
    {
        // Ignore read errors as the servo might not respond
        return;
    }
}

int16_t ST3215ServoReader::read_load(uint8_t servo_id)
{
    for (int retry = 0; retry < communication::MAX_RETRIES; ++retry)
    {
        try
        {
            // Create read load command packet
            std::vector<uint8_t> command = _create_read_command(servo_id, register_addr::PRESENT_LOAD, 2);

            // Clear any existing data
            ::tcflush(static_cast<int>(_serial_port.native_handle()), TCIOFLUSH);
            std::this_thread::sleep_for(timing::COMMAND_INTERVAL);

            // Send command
            boost::system::error_code write_ec;
            size_t written = boost::asio::write(_serial_port, buffer(command), write_ec);

            if (write_ec || written != command.size())
            {
                throw std::runtime_error("Failed to write command");
            }

            // Ensure minimum response time
            std::this_thread::sleep_for(timing::MIN_RESPONSE_TIME);

            // Read response
            std::array<uint8_t, 8> response;  // Header(4) + Error(1) + Data(2) + Checksum(1)
            size_t read = boost::asio::read(_serial_port, buffer(response), write_ec);

            if (write_ec || read != response.size())
            {
                throw std::runtime_error("Failed to read response");
            }

            // Validate response
            if (response[0] != protocol::HEADER1 || response[1] != protocol::HEADER2 || response[2] != servo_id)
            {
                throw std::runtime_error("Invalid response header");
            }

            // Check for servo errors
            if (response[4] != 0x00)
            {
                std::stringstream ss;
                ss << "Servo reported error 0x" << std::hex << static_cast<int>(response[4]);
                throw std::runtime_error(ss.str());
            }

            // Load value is in little-endian format
            return static_cast<int16_t>(response[5] | (response[6] << 8));
        }
        catch (const std::runtime_error& e)
        {
            if (retry == communication::MAX_RETRIES - 1)
            {
                throw;
            }
            ::tcflush(static_cast<int>(_serial_port.native_handle()), TCIOFLUSH);
            std::this_thread::sleep_for(timing::RETRY_DELAY);
        }
    }
    throw std::runtime_error("Maximum retries exceeded");
}