// Copyright 2025 Enactic, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include std::atomic keep_running(true); constexpr std::size_t SENSOR_JOINT_NUM = 7; std::array, SENSOR_JOINT_NUM> sensor_angle_rad; std::array, SENSOR_JOINT_NUM> sensor_velocity_rad_s; std::array, SENSOR_JOINT_NUM> sensor_angle_ready; struct openarm_joint_sensor_status { uint8_t id; double angle_rad; double velocity_rad_s; double torque_Nm; }; constexpr std::array SENSOR_CAN_ID = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}; // Per-joint sign: use +1.0 or -1.0 to match each joint's positive rotation direction. constexpr std::array SENSOR_DIRECTION_SIGN = { -1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0}; // Per-joint zero offset in radians: joint_angle = sign * raw_angle + offset. constexpr std::array SENSOR_ZERO_OFFSET_RAD = { 2.75, -2.49, 0.08, 0.0, 0.0, 0.0, 0.0}; // constexpr double SENSOR_SCALE = 1e-3; // TODO: replace with your sensor scaling // constexpr double SENSOR_OFFSET = 0.0; constexpr double SENSOR_FILTER_ALPHA = 0.05; // 0~1, larger = less smoothing constexpr double SENSOR_VEL_FILTER_ALPHA = 0.2; // 0~1, larger = less smoothing constexpr double pi = 3.14159265358979323846; constexpr double TWO_PI = 2.0 * pi; double wrap_to_pi(double angle_rad) { return std::atan2(std::sin(angle_rad), std::cos(angle_rad)); } double convert_0_to_2pi_to_signed(double angle_rad_0_to_2pi) { double angle = std::fmod(angle_rad_0_to_2pi, TWO_PI); if (angle < 0.0) { angle += TWO_PI; } if (angle > pi) { angle -= TWO_PI; } return angle; } int sensor_id_to_joint_index(canid_t id) { for (std::size_t i = 0; i < SENSOR_CAN_ID.size(); ++i) { if (SENSOR_CAN_ID[i] == id) { return static_cast(i); } } return -1; } int open_can_socket(const std::string &can_interface) { int sock = socket(PF_CAN, SOCK_RAW, CAN_RAW); if (sock < 0) { throw std::runtime_error("Failed to open CAN socket"); } struct ifreq ifr; std::snprintf(ifr.ifr_name, IFNAMSIZ, "%s", can_interface.c_str()); if (ioctl(sock, SIOCGIFINDEX, &ifr) < 0) { close(sock); throw std::runtime_error("Failed to get CAN interface index"); } struct sockaddr_can addr; std::memset(&addr, 0, sizeof(addr)); addr.can_family = AF_CAN; addr.can_ifindex = ifr.ifr_ifindex; if (bind(sock, reinterpret_cast(&addr), sizeof(addr)) < 0) { close(sock); throw std::runtime_error("Failed to bind CAN socket"); } struct can_filter filter; filter.can_id = 0x01; filter.can_mask = CAN_SFF_MASK & 0xF0; // filter.can_mask = CAN_SFF_MASK; if (setsockopt(sock, SOL_CAN_RAW, CAN_RAW_FILTER, &filter, sizeof(filter)) < 0) { close(sock); throw std::runtime_error("Failed to set CAN filter"); } return sock; } bool read_sensor(int sock, openarm_joint_sensor_status &status_out) { struct pollfd pfd; pfd.fd = sock; pfd.events = POLLIN; int ret = poll(&pfd, 1, 10); if (ret <= 0 || !(pfd.revents & POLLIN)) { return false; } struct can_frame frame; int nbytes = read(sock, &frame, sizeof(frame)); if (nbytes != static_cast(sizeof(frame))) { return false; } const canid_t frame_id = frame.can_id & CAN_SFF_MASK; bool valid_id = false; for (const auto &id : SENSOR_CAN_ID) { if (frame_id == id) { valid_id = true; break; } // std::cout << "Received CAN ID: " << frame_id << ", expected: " << id << std::endl; } if (!valid_id || frame.can_dlc < 5) { return false; } int32_t rawPos = 0; rawPos |= static_cast(frame.data[1]) << 8; rawPos |= static_cast(frame.data[2]); int16_t rawVel = 0; rawVel |= static_cast(frame.data[3]) << 8; rawVel |= static_cast(frame.data[4]); status_out.id = frame_id; status_out.velocity_rad_s = (static_cast(rawVel) / 100.0 / 4096.0) * 360.0 * (pi / 180.0); // Sensor reports angle in [0, 2pi). Keep it as-is here and normalize later. status_out.angle_rad = (static_cast(rawPos) / 4096.0) * TWO_PI; std::cout << "CAN ID: " << frame_id << " angle: " << status_out.angle_rad << " velocity: " << status_out.velocity_rad_s << std::endl; return true; } void signal_handler(int signal) { if (signal == SIGINT) { std::cout << "\nCtrl+C detected. Exiting loop..." << std::endl; keep_running = false; } } class FollowerArmThread : public PeriodicTimerThread { public: FollowerArmThread(std::shared_ptr robot_state, Control *control_f, std::size_t joint_num, double hz = 500.0) : PeriodicTimerThread(hz), robot_state_(robot_state), control_f_(control_f), joint_num_(joint_num) {} protected: void before_start() override { std::cout << "follower start thread " << std::endl; } void after_stop() override { std::cout << "follower stop thread " << std::endl; } void on_timer() override { static auto prev_time = std::chrono::steady_clock::now(); for (std::size_t i = 0; i < joint_num_; ++i) { if (!sensor_angle_ready[i].load()) { continue; } control_f_->SetArmJointReference(static_cast(i), sensor_angle_rad[i].load(), sensor_velocity_rad_s[i].load()); // std::cout << "Joint " << i << " angle: " << sensor_angle_rad[i].load() // << " velocity: " << sensor_velocity_rad_s[i].load() << std::endl; } control_f_->unilateral_step(); auto now = std::chrono::steady_clock::now(); auto elapsed_us = std::chrono::duration_cast(now - prev_time).count(); prev_time = now; // std::cout << "Leader joint 0 position: " << robot_state_->arm_state().get_reference(0).position // << std::endl; // std::cout << "Leader joint 0 velocity: " << robot_state_->arm_state().get_reference(0).velocity // << std::endl; // std::cout << "[Follower] Period: " << elapsed_us << " us" << std::endl; } private: std::shared_ptr robot_state_; Control *control_f_; std::size_t joint_num_; }; int main(int argc, char **argv) { try { std::signal(SIGINT, signal_handler); for (std::size_t i = 0; i < SENSOR_JOINT_NUM; ++i) { sensor_angle_rad[i].store(0.0); sensor_velocity_rad_s[i].store(0.0); sensor_angle_ready[i].store(false); } // default configration std::string arm_side = "right_arm"; std::string leader_urdf_path; std::string follower_urdf_path; std::string follower_can_interface = "can2"; std::string sensor_can_interface = "can0"; if (argc < 3) { std::cerr << "Usage: " << argv[0] << " [arm_side] [leader_can] [follower_can]" << std::endl; return 1; } // Required: URDF paths leader_urdf_path = argv[1]; follower_urdf_path = argv[2]; // Optional: arm_side if (argc >= 4) { arm_side = argv[3]; if (arm_side != "left_arm" && arm_side != "right_arm") { std::cerr << "[ERROR] Invalid arm_side: " << arm_side << ". Must be 'left_arm' or 'right_arm'." << std::endl; return 1; } } // Optional: CAN interfaces if (argc >= 6) { follower_can_interface = argv[5]; } // Optional: sensor CAN interface if (argc >= 7) { sensor_can_interface = argv[6]; } // URDF file existence check if (!std::filesystem::exists(leader_urdf_path)) { std::cerr << "[ERROR] Leader URDF not found: " << leader_urdf_path << std::endl; return 1; } if (!std::filesystem::exists(follower_urdf_path)) { std::cerr << "[ERROR] Follower URDF not found: " << follower_urdf_path << std::endl; return 1; } // Setup dynamics std::string root_link = "openarm_body_link0"; std::string leaf_link = (arm_side == "left_arm") ? "openarm_left_hand" : "openarm_right_hand"; // Output confirmation std::cout << "=== OpenArm Unilateral Control ===" << std::endl; std::cout << "Arm side : " << arm_side << std::endl; std::cout << "Follower CAN : " << follower_can_interface << std::endl; std::cout << "Leader URDF path : " << leader_urdf_path << std::endl; std::cout << "Follower URDF path: " << follower_urdf_path << std::endl; std::cout << "Root link : " << root_link << std::endl; std::cout << "Leaf link : " << leaf_link << std::endl; std::cout << "Sensor CAN : " << sensor_can_interface << std::endl; int sensor_can_socket = open_can_socket(sensor_can_interface); std::thread can_read_thread([&]() { std::array filtered_angle{}; std::array filtered_velocity{}; std::array filter_initialized{}; openarm_joint_sensor_status status; while (keep_running) { if (read_sensor(sensor_can_socket, status)) { const int joint_idx = sensor_id_to_joint_index(status.id); if (joint_idx < 0) { continue; } const std::size_t i = static_cast(joint_idx); const double sensor_angle_signed = convert_0_to_2pi_to_signed(status.angle_rad); const double angle = wrap_to_pi( SENSOR_DIRECTION_SIGN[i] * sensor_angle_signed + SENSOR_ZERO_OFFSET_RAD[i]); const double velocity = SENSOR_DIRECTION_SIGN[i] * status.velocity_rad_s; if (!filter_initialized[i]) { filtered_angle[i] = angle; filtered_velocity[i] = velocity; filter_initialized[i] = true; } else { filtered_angle[i] = SENSOR_FILTER_ALPHA * angle + (1.0 - SENSOR_FILTER_ALPHA) * filtered_angle[i]; filtered_velocity[i] = SENSOR_VEL_FILTER_ALPHA * velocity + (1.0 - SENSOR_VEL_FILTER_ALPHA) * filtered_velocity[i]; } sensor_angle_rad[i].store(filtered_angle[i]); sensor_velocity_rad_s[i].store(filtered_velocity[i]); sensor_angle_ready[i].store(true); std::cout << "Joint " << i << " filtered_angle: " << filtered_angle[i] << ", filtered_velocity: " << filtered_velocity[i] << std::endl; } } }); YamlLoader leader_loader("/home/shen/openarm_ros2_ws/src/openarm_teleop/config/leader.yaml"); YamlLoader follower_loader("/home/shen/openarm_ros2_ws/src/openarm_teleop/config/follower.yaml"); // Follower parameters std::vector follower_kp = follower_loader.get_vector("FollowerArmParam", "Kp"); std::vector follower_kd = follower_loader.get_vector("FollowerArmParam", "Kd"); std::vector follower_Fc = follower_loader.get_vector("FollowerArmParam", "Fc"); std::vector follower_k = follower_loader.get_vector("FollowerArmParam", "k"); std::vector follower_Fv = follower_loader.get_vector("FollowerArmParam", "Fv"); std::vector follower_Fo = follower_loader.get_vector("FollowerArmParam", "Fo"); Dynamics *leader_arm_dynamics = new Dynamics(leader_urdf_path, root_link, leaf_link); leader_arm_dynamics->Init(); Dynamics *follower_arm_dynamics = new Dynamics(follower_urdf_path, root_link, leaf_link); follower_arm_dynamics->Init(); std::cout << "=== Initializing Follower OpenArm ===" << std::endl; openarm::can::socket::OpenArm *follower_openarm = openarm_init::OpenArmInitializer::initialize_openarm(follower_can_interface, true); size_t follower_arm_motor_num = follower_openarm->get_arm().get_motors().size(); size_t follower_hand_motor_num = follower_openarm->get_gripper().get_motors().size(); std::cout << "follower arm motor num : " << follower_arm_motor_num << std::endl; std::cout << "follower hand motor num : " << follower_hand_motor_num << std::endl; // Declare robot_state (Joint and motor counts are assumed to be equal) std::shared_ptr follower_state = std::make_shared(follower_arm_motor_num, follower_hand_motor_num); Control *control_follower = new Control(follower_openarm, leader_arm_dynamics, follower_arm_dynamics, follower_state, 1.0 / FREQUENCY, ROLE_FOLLOWER, arm_side, follower_arm_motor_num, follower_hand_motor_num); control_follower->SetParameter(follower_kp, follower_kd, follower_Fc, follower_k, follower_Fv, follower_Fo); // set home postion // std::thread thread_l(&Control::AdjustPosition, control_leader); // std::thread thread_f(&Control::AdjustPosition, control_follower); // thread_l.join(); // thread_f.join(); // Start control process const std::size_t controlled_joint_num = std::min(follower_arm_motor_num, SENSOR_JOINT_NUM); FollowerArmThread follower_thread(follower_state, control_follower, controlled_joint_num, FREQUENCY); follower_thread.start_thread(); while (keep_running) { std::this_thread::sleep_for(std::chrono::milliseconds(1000)); } follower_thread.stop_thread(); follower_openarm->disable_all(); if (can_read_thread.joinable()) { can_read_thread.join(); } close(sensor_can_socket); } catch (const std::exception &e) { std::cerr << e.what() << '\n'; } return 0; }