0편 – [robot arm, manipulator 프로젝트][ubuntu 20.04, ros2, gazebo][0] – ros2, gazebo 설치 및 예제 실행
1편 – [robot arm, manipulator 프로젝트][ubuntu 20.04, ros2, gazebo][1] – ros2 publisher subscriber c++, python 구현 및 실행

1. 아두이노 IDE & OpenCR 개발환경

image 72

링크 에서 아두이노 설치 파일 다운

다운로드된 .Appimage 확장자의 파일은 더블 클릭 하거나 터미널에서 ./arduino~~.Appimage 를 입력하면 실행된다.

이후 터미널에서 아래와 같이 아두이노에서 사용할 포트 관련 설정을 포함한 기타 작업을 진행해준다.

wget https://raw.githubusercontent.com/ROBOTIS-GIT/OpenCR/master/99-opencr-cdc.rules
sudo cp ./99-opencr-cdc.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules
sudo udevadm trigger
sudo apt-get install libncurses5-dev:i386
image 73

위 사진과 같이 설정의 preferences 에서 additional boards manager URLs 추가

https://raw.githubusercontent.com/ROBOTIS-GIT/OpenCR/master/arduino/opencr_release/package_opencr_index.json
image 74

tools->board->boards manager 에서 opencr 보드 검색하여 install

image 75

위 사진과 같이 board 와 port 를 설정해주면 연결이 잘 된 것을 볼 수 있다.

2. ROS2 와 아두이노 통신

microROS 활용하여 통신 해보자

2.1. 환경 설정

먼저 본인의 ros2 workspace 에서 작업 시작

cd ~/ros2_ws/src
git clone -b $ROS_DISTRO https://github.com/micro-ROS/micro_ros_setup.git
sudo apt update && rosdep update
rosdep install --from-paths src --ignore-src -y

cd ~/ros2_ws
colcon build
source install/local_setup.bash

cd ~/ros2_ws
ros2 run micro_ros_setup create_agent_ws.sh

ros2 run micro_ros_setup build_agent.sh
source install/local_setup.bash

cd ~/ros2_ws
colcon build
source install/local_setup.bash

ros2 run micro_ros_agent micro_ros_agent serial --dev /dev/ttyACM0
image 77

위의 사진처럼 된다면 성공

이후 아두이노에서 micro-ros 라이브러리 설치

링크에서 본인 ros 버전에 맞는 zip 파일 다운로드

image 78

아두이노 ide 에서 Sketch->include library-> add .ZIP Library 를 통해 다운로드한 라이브러리 추가

2.2. Publisher

file-> example-> micro_ros_arduino 에서 publisher 예제를 컴파일&업로드 한다. 만약 실패한다면

/home/jw/.arduino15/packages/arduino/hardware/avr/1.8.6/
or
/home/jw/.Arduino15/packages/arduino/hardware/sam/1.6.12/

위와 같은 경로에서 터미널을 열고

curl https://raw.githubusercontent.com/micro-ROS/micro_ros_arduino/main/extras/patching_boards/platform_arduinocore_sam.txt > platform.txt

위 코드를 실행한다. 이후

각각의 새로운 터미널 2개에서

ros2 run micro_ros_agent micro_ros_agent serial --dev /dev/ttyACM0

ros2 topic echo /micro_ros_arduino_node_publisher

위에 코드를 각각 입력하면 아래와 같은 결과를 얻을 수 있다.

image 79
image 80

rqt 를 통해서도 확인 할 수 있다.

결국 위에 작업은 추후 모터에서 얻을 엔코더 각도, 각속도, 전류 값을 ros2 로 보내주기 위한 작업이다.

2.3. subscriber

아두이노 ide 에서 file->examples->micro_ros_arduino 에서 subscriber 예제를 업로드 한다.

이때 터미널에서 ros2 run micro_ros_agent micro_ros_agent serial –dev /dev/ttyACM0 는 실행되고 있어야 한다.

rqt 에 plugins -> message publisher 를 열고 Hz 옆에 + 버튼을 눌러준다.
아래에 topic 이 추가되면 expression 에 1 혹은 0을 입력한뒤 오른쪽 마우스 클릭하여 published selected once 를 누르면 1인지 0인지에 따라 보드에서 led 가 켜지거나 꺼진다.

image 81
image 82
image 83

결국 이 작업은 원하는 각도록 보드에 연결된 모터를 제어하기 위해 필요하다.

2.4. sub & pub 동시에

결국에는

ros -> 모터 각도값 -> opencr -> 모터
ros <- opencr<- 모터 엔코더 값, 전류 기타 등등

이러한 형태가 되어야 하기 때문에 sub 과 pub 이 따로따로 수행되면 안된다.

때문에 subscribe 할 때마다 Publish 하는 코드를 작성하고 테스트 하였다.

#include <micro_ros_arduino.h>

#include <stdio.h>
#include <rcl/rcl.h>
#include <rcl/error_handling.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>

#include <std_msgs/msg/int32.h>

rcl_subscription_t subscriber;
rcl_publisher_t publisher;
std_msgs__msg__Int32 sub_msg;
std_msgs__msg__Int32 pub_msg;

rclc_executor_t executor;
rclc_support_t support;
rcl_allocator_t allocator;
rcl_node_t node;

#define LED_PIN 13

#define RCCHECK(fn) { rcl_ret_t temp_rc = fn; if((temp_rc != RCL_RET_OK)){error_loop();}}
#define RCSOFTCHECK(fn) { rcl_ret_t temp_rc = fn; if((temp_rc != RCL_RET_OK)){}}

void error_loop() {
  while (1) {
    digitalWrite(LED_PIN, !digitalRead(LED_PIN));
    delay(100);
  }
}

void subscription_callback(const void *msgin) {  
  const std_msgs__msg__Int32 *msg = (const std_msgs__msg__Int32 *)msgin;

  // Toggle LED state based on the received message
  if (msg->data == 0) {
    digitalWrite(22, LOW); // Turn LED OFF
  } else {
    digitalWrite(22, HIGH); // Turn LED ON
  }

  // Publish the incremented value
  pub_msg.data++;
  RCSOFTCHECK(rcl_publish(&publisher, &pub_msg, NULL));
}

void setup() {
  set_microros_transports();

  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, HIGH);  

  delay(2000);

  allocator = rcl_get_default_allocator();

  // Initialize support
  RCCHECK(rclc_support_init(&support, 0, NULL, &allocator));

  // Create node
  RCCHECK(rclc_node_init_default(&node, "micro_ros_arduino_node", "", &support));

  // Create subscriber
  RCCHECK(rclc_subscription_init_default(
    &subscriber,
    &node,
    ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Int32),
    "micro_ros_arduino_subscriber"));

  // Create publisher
  RCCHECK(rclc_publisher_init_default(
    &publisher,
    &node,
    ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Int32),
    "micro_ros_arduino_publisher"));

  // Create executor
  RCCHECK(rclc_executor_init(&executor, &support.context, 2, &allocator));
  RCCHECK(rclc_executor_add_subscription(&executor, &subscriber, &sub_msg, &subscription_callback, ON_NEW_DATA));

  // Initialize publisher message
  pub_msg.data = 0;
}

void loop() {
  RCCHECK(rclc_executor_spin_some(&executor, RCL_MS_TO_NS(100)));
  delay(100); // Small delay to avoid busy loop
}

위 코드를 통해 0 혹은 1 값을 sub 하여 led 를 키거나 끄고 이와 동시에 data 를 sub 할 때마다 1 씩 증가하는 data를 pub 하는 것을 확인하였다.

image 87

3. 아두이노(OpenCR 보드) 와 모터(Dynamixel) 통신

구체적으로는 로보티즈 회사의 xl430 모터를 다루고 있다.

먼저 로보티즈 사의DYNAMIXEL Wizard 2.0 를 통해 각각의 모터를 돌려보았다.

링크

image 85
image 84

2개의 xl430 모터의 id 를 확인 하고 다르게 설정해주었다.

#include <DynamixelSDK.h>

// Control Table Address (XL430)
#define ADDR_XL_TORQUE_ENABLE          64  // Torque Enable Address
#define ADDR_XL_GOAL_POSITION          116 // Goal Position Address
#define ADDR_XL_PRESENT_POSITION       132 // Present Position Address

// Data Byte Length
#define LEN_XL_GOAL_POSITION           4
#define LEN_XL_PRESENT_POSITION        4

// Protocol version
#define PROTOCOL_VERSION                2.0 // XL430 uses Protocol 2.0

// Default setting
#define DXL1_ID                         13   // Dynamixel#1 ID
#define DXL2_ID                         14   // Dynamixel#2 ID
#define BAUDRATE                        57600 // Baudrate
#define DEVICENAME                      "OpenCR_DXL_Port" // Serial Port

#define TORQUE_ENABLE                   1    // Value for enabling the torque
#define TORQUE_DISABLE                  0    // Value for disabling the torque
#define DXL_MINIMUM_POSITION_VALUE      0    // Minimum position value (in pulse)
#define DXL_MAXIMUM_POSITION_VALUE      4095 // Maximum position value (in pulse)
#define DXL_MOVING_STATUS_THRESHOLD     10   // Moving status threshold

#define CMD_SERIAL                      Serial

int getch()
{
  while (1)
  {
    if (CMD_SERIAL.available() > 0)
    {
      break;
    }
  }
  return CMD_SERIAL.read();
}

void setup()
{
  CMD_SERIAL.begin(115200);
  while (!CMD_SERIAL);

  CMD_SERIAL.println("Start...");

  // Initialize PortHandler instance
  dynamixel::PortHandler *portHandler = dynamixel::PortHandler::getPortHandler(DEVICENAME);

  // Initialize PacketHandler instance
  dynamixel::PacketHandler *packetHandler = dynamixel::PacketHandler::getPacketHandler(PROTOCOL_VERSION);

  // Initialize GroupSyncWrite and GroupSyncRead instances
  dynamixel::GroupSyncWrite groupSyncWrite(portHandler, packetHandler, ADDR_XL_GOAL_POSITION, LEN_XL_GOAL_POSITION);
  dynamixel::GroupSyncRead groupSyncRead(portHandler, packetHandler, ADDR_XL_PRESENT_POSITION, LEN_XL_PRESENT_POSITION);

  int dxl_comm_result;
  uint8_t dxl_error;
  int32_t dxl1_present_position, dxl2_present_position;
  uint8_t param_goal_position[4];
  int dxl_goal_position[2] = {DXL_MINIMUM_POSITION_VALUE, DXL_MAXIMUM_POSITION_VALUE};
  int index = 0;

  // Open port
  if (!portHandler->openPort() || !portHandler->setBaudRate(BAUDRATE))
  {
    CMD_SERIAL.println("Failed to open the port or set baudrate.");
    return;
  }

  CMD_SERIAL.println("Port opened!");

  // Enable Torque for both motors
  for (int id : {DXL1_ID, DXL2_ID})
  {
    dxl_comm_result = packetHandler->write1ByteTxRx(portHandler, id, ADDR_XL_TORQUE_ENABLE, TORQUE_ENABLE, &dxl_error);
    if (dxl_comm_result != COMM_SUCCESS || dxl_error != 0)
    {
      CMD_SERIAL.print("[ID:"); CMD_SERIAL.print(id); CMD_SERIAL.println("] Torque Enable failed.");
      return;
    }
  }

  CMD_SERIAL.println("Motors connected!");

  // Add both motors to GroupSyncRead
  if (!groupSyncRead.addParam(DXL1_ID) || !groupSyncRead.addParam(DXL2_ID))
  {
    CMD_SERIAL.println("Failed to add parameters to GroupSyncRead.");
    return;
  }

  while (1)
  {
    CMD_SERIAL.println("Press any key to continue! (or press 'q' to quit)");
    if (getch() == 'q')
      break;

    // Prepare Goal Position parameters
    for (int id : {DXL1_ID, DXL2_ID})
    {
      param_goal_position[0] = DXL_LOBYTE(DXL_LOWORD(dxl_goal_position[index]));
      param_goal_position[1] = DXL_HIBYTE(DXL_LOWORD(dxl_goal_position[index]));
      param_goal_position[2] = DXL_LOBYTE(DXL_HIWORD(dxl_goal_position[index]));
      param_goal_position[3] = DXL_HIBYTE(DXL_HIWORD(dxl_goal_position[index]));

      if (!groupSyncWrite.addParam(id, param_goal_position))
      {
        CMD_SERIAL.print("[ID:"); CMD_SERIAL.print(id); CMD_SERIAL.println("] Failed to add goal position.");
        return;
      }
    }

    // Send Goal Position to both motors
    dxl_comm_result = groupSyncWrite.txPacket();
    if (dxl_comm_result != COMM_SUCCESS)
    {
      CMD_SERIAL.println(packetHandler->getTxRxResult(dxl_comm_result));
    }
    groupSyncWrite.clearParam();

    // Wait for motors to reach the goal
    do
    {
      dxl_comm_result = groupSyncRead.txRxPacket();
      if (dxl_comm_result != COMM_SUCCESS)
      {
        CMD_SERIAL.println(packetHandler->getTxRxResult(dxl_comm_result));
        break;
      }

      // Retrieve Present Position for both motors
      dxl1_present_position = groupSyncRead.getData(DXL1_ID, ADDR_XL_PRESENT_POSITION, LEN_XL_PRESENT_POSITION);
      dxl2_present_position = groupSyncRead.getData(DXL2_ID, ADDR_XL_PRESENT_POSITION, LEN_XL_PRESENT_POSITION);

      CMD_SERIAL.print("[ID:"); CMD_SERIAL.print(DXL1_ID);
      CMD_SERIAL.print("] Present Position: "); CMD_SERIAL.print(dxl1_present_position);
      CMD_SERIAL.print("  [ID:"); CMD_SERIAL.print(DXL2_ID);
      CMD_SERIAL.print("] Present Position: "); CMD_SERIAL.println(dxl2_present_position);

    } while (abs(dxl_goal_position[index] - dxl1_present_position) > DXL_MOVING_STATUS_THRESHOLD ||
             abs(dxl_goal_position[index] - dxl2_present_position) > DXL_MOVING_STATUS_THRESHOLD);

    index = (index == 0) ? 1 : 0; // Toggle goal position
  }

  // Disable Torque for both motors
  for (int id : {DXL1_ID, DXL2_ID})
  {
    dxl_comm_result = packetHandler->write1ByteTxRx(portHandler, id, ADDR_XL_TORQUE_ENABLE, TORQUE_DISABLE, &dxl_error);
    if (dxl_comm_result != COMM_SUCCESS || dxl_error != 0)
    {
      CMD_SERIAL.print("[ID:"); CMD_SERIAL.print(id); CMD_SERIAL.println("] Torque Disable failed.");
    }
  }

  // Close port
  portHandler->closePort();
  CMD_SERIAL.println("Port closed!");
}

void loop()
{
}

4. 최종 ROS2, OpenCR, Dynamixel 모터(xl430) 통신

#include <micro_ros_arduino.h>
#include <DynamixelSDK.h>

#include <stdio.h>
#include <rcl/rcl.h>
#include <rcl/error_handling.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>

// Include the required message types
#include <std_msgs/msg/int32.h>
#include <std_msgs/msg/int32_multi_array.h>

rcl_subscription_t subscriber;
rcl_publisher_t publisher;
std_msgs__msg__Int32 sub_msg;
std_msgs__msg__Int32MultiArray pub_msg;

rclc_executor_t executor;
rclc_support_t support;
rcl_allocator_t allocator;
rcl_node_t node;

#define LED_PIN 13

// Control Table Address (XL430)
#define ADDR_XL_TORQUE_ENABLE          64  // Torque Enable Address
#define ADDR_XL_GOAL_POSITION          116 // Goal Position Address
#define ADDR_XL_PRESENT_POSITION       132 // Present Position Address

// Data Byte Length
#define LEN_XL_GOAL_POSITION           4
#define LEN_XL_PRESENT_POSITION        4

// Protocol version
#define PROTOCOL_VERSION                2.0 // XL430 uses Protocol 2.0

// Default setting
#define DXL1_ID                         13   // Dynamixel#1 ID
#define DXL2_ID                         14   // Dynamixel#2 ID
#define BAUDRATE                        57600 // Baudrate
#define DEVICENAME                      "OpenCR_DXL_Port" // Serial Port

#define TORQUE_ENABLE                   1    // Value for enabling the torque
#define TORQUE_DISABLE                  0    // Value for disabling the torque
#define DXL_MINIMUM_POSITION_VALUE      0    // Minimum position value (in pulse)
#define DXL_MAXIMUM_POSITION_VALUE      4095 // Maximum position value (in pulse)
#define DXL_MOVING_STATUS_THRESHOLD     10   // Moving status threshold

#define RCCHECK(fn) { rcl_ret_t temp_rc = fn; if((temp_rc != RCL_RET_OK)){error_loop();}}
#define RCSOFTCHECK(fn) { rcl_ret_t temp_rc = fn; if((temp_rc != RCL_RET_OK)){}}

dynamixel::PortHandler *portHandler;
dynamixel::PacketHandler *packetHandler;
dynamixel::GroupSyncWrite *groupSyncWrite;
dynamixel::GroupSyncRead *groupSyncRead;

int32_t dxl_present_positions[2];

void error_loop() {
  while (1) {
    digitalWrite(LED_PIN, !digitalRead(LED_PIN));
    delay(100);
  }
}

// Helper to clamp values
int clamp(int value, int min_value, int max_value) {
  if (value < min_value) return min_value;
  if (value > max_value) return max_value;
  return value;
}

// Subscription Callback
void subscription_callback(const void *msgin) {  
  const std_msgs__msg__Int32 *msg = (const std_msgs__msg__Int32 *)msgin;

  // Clamp the received value within the allowed range
  int target_position = clamp(msg->data, DXL_MINIMUM_POSITION_VALUE, DXL_MAXIMUM_POSITION_VALUE);

  // Prepare goal position parameter
  uint8_t param_goal_position[4];
  param_goal_position[0] = DXL_LOBYTE(DXL_LOWORD(target_position));
  param_goal_position[1] = DXL_HIBYTE(DXL_LOWORD(target_position));
  param_goal_position[2] = DXL_LOBYTE(DXL_HIWORD(target_position));
  param_goal_position[3] = DXL_HIBYTE(DXL_HIWORD(target_position));

  // Add goal position for DXL1 and DXL2
  groupSyncWrite->addParam(DXL1_ID, param_goal_position);
  groupSyncWrite->addParam(DXL2_ID, param_goal_position);

  // Write goal positions
  groupSyncWrite->txPacket();
  groupSyncWrite->clearParam();

  // Read current positions
  groupSyncRead->txRxPacket();
  dxl_present_positions[0] = groupSyncRead->getData(DXL1_ID, ADDR_XL_PRESENT_POSITION, LEN_XL_PRESENT_POSITION);
  dxl_present_positions[1] = groupSyncRead->getData(DXL2_ID, ADDR_XL_PRESENT_POSITION, LEN_XL_PRESENT_POSITION);

  // Publish the current positions as an array
  pub_msg.data.data[0] = dxl_present_positions[0];
  pub_msg.data.data[1] = dxl_present_positions[1];
  RCSOFTCHECK(rcl_publish(&publisher, &pub_msg, NULL));
}

void setup() {
  // Setup ROS transport
  set_microros_transports();

  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, HIGH);  

  delay(2000);

  allocator = rcl_get_default_allocator();

  // Initialize support
  RCCHECK(rclc_support_init(&support, 0, NULL, &allocator));

  // Create node
  RCCHECK(rclc_node_init_default(&node, "micro_ros_arduino_node", "", &support));

  // Create subscriber
  RCCHECK(rclc_subscription_init_default(
    &subscriber,
    &node,
    ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Int32),
    "micro_ros_arduino_subscriber"));

  // Create publisher
  RCCHECK(rclc_publisher_init_default(
    &publisher,
    &node,
    ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Int32MultiArray),
    "micro_ros_arduino_publisher"));

  // Create executor
  RCCHECK(rclc_executor_init(&executor, &support.context, 2, &allocator));
  RCCHECK(rclc_executor_add_subscription(&executor, &subscriber, &sub_msg, &subscription_callback, ON_NEW_DATA));

  // Initialize Dynamixel Communication
  portHandler = dynamixel::PortHandler::getPortHandler(DEVICENAME);
  packetHandler = dynamixel::PacketHandler::getPacketHandler(PROTOCOL_VERSION);

  groupSyncWrite = new dynamixel::GroupSyncWrite(portHandler, packetHandler, ADDR_XL_GOAL_POSITION, LEN_XL_GOAL_POSITION);
  groupSyncRead = new dynamixel::GroupSyncRead(portHandler, packetHandler, ADDR_XL_PRESENT_POSITION, LEN_XL_PRESENT_POSITION);

  if (!portHandler->openPort()) error_loop();
  if (!portHandler->setBaudRate(BAUDRATE)) error_loop();

  uint8_t dxl_error;
  int dxl_comm_result;

  // Enable motor torque
  for (int id : {DXL1_ID, DXL2_ID}) {
    dxl_comm_result = packetHandler->write1ByteTxRx(portHandler, id, ADDR_XL_TORQUE_ENABLE, TORQUE_ENABLE, &dxl_error);
    if (dxl_comm_result != COMM_SUCCESS || dxl_error != 0) error_loop();
    groupSyncRead->addParam(id);
  }

  // Initialize publisher message
  pub_msg.data.data = (int32_t *)malloc(2 * sizeof(int32_t)); // Allocate memory for two integers
  pub_msg.data.size = 2; // Array size
  pub_msg.data.capacity = 2; // Array capacity
}

void loop() {
  RCCHECK(rclc_executor_spin_some(&executor, RCL_MS_TO_NS(100)));
  delay(100); // Small delay to avoid busy loop
}

위에 코드를 통해 sub 하는 값으로 모터를 움직이고, 모터에서 얻은 엔코더 값을 pub 하는 것을 구현하였다.

image 88

message publisher 를 통해 xl430 모터의 동작 범위 0~4028 내에서 값을 입력하여 메세지를 보내면 이를 openCR 이 sub 하고 모터가 움직인다. 그리고 해당 모터의 엔코더 값을 읽고 각 모터의 엔코더 값을 pub 한다.


0 Comments

Leave a Reply