From 7fcf7ff03a625cd89b1f09881d4d23b2e1ef2218 Mon Sep 17 00:00:00 2001 From: Dwarakesh Baraneetharan Date: Tue, 6 Jan 2026 16:15:16 -0500 Subject: [PATCH] Add laser, killswitch, and LED hardware interfaces --- .../science.killswitch.ros2_control.xacro | 33 ++ .../science/science.laser.ros2_control.xacro | 28 + .../science/science.led.ros2_control.xacro | 25 + .../urdf/athena_science.urdf.xacro | 19 +- src/hardware_interfaces/README.md | 8 +- .../killswitch_ros2_control/CMakeLists.txt | 69 +++ .../killswitch_hardware_interface.hpp | 134 +++++ .../killswitch_hardware_interface.xml | 10 + .../killswitch_ros2_control/package.xml | 26 + .../src/killswitch_hardware_interface.cpp | 483 ++++++++++++++++++ .../laser_ros2_control/CMakeLists.txt | 69 +++ .../laser_hardware_interface.hpp | 118 +++++ .../laser_hardware_interface.xml | 10 + .../laser_ros2_control/package.xml | 26 + .../src/laser_hardware_interface.cpp | 338 ++++++++++++ .../led_ros2_control/CMakeLists.txt | 69 +++ .../led_hardware_interface.hpp | 112 ++++ .../led_hardware_interface.xml | 10 + .../led_ros2_control/package.xml | 26 + .../src/led_hardware_interface.cpp | 312 +++++++++++ .../config/athena_science_controllers.yaml | 62 ++- 21 files changed, 1983 insertions(+), 4 deletions(-) create mode 100644 src/description/ros2_control/science/science.killswitch.ros2_control.xacro create mode 100644 src/description/ros2_control/science/science.laser.ros2_control.xacro create mode 100644 src/description/ros2_control/science/science.led.ros2_control.xacro create mode 100644 src/hardware_interfaces/killswitch_ros2_control/CMakeLists.txt create mode 100644 src/hardware_interfaces/killswitch_ros2_control/include/killswitch_ros2_control/killswitch_hardware_interface.hpp create mode 100644 src/hardware_interfaces/killswitch_ros2_control/killswitch_hardware_interface.xml create mode 100644 src/hardware_interfaces/killswitch_ros2_control/package.xml create mode 100644 src/hardware_interfaces/killswitch_ros2_control/src/killswitch_hardware_interface.cpp create mode 100644 src/hardware_interfaces/laser_ros2_control/CMakeLists.txt create mode 100644 src/hardware_interfaces/laser_ros2_control/include/laser_ros2_control/laser_hardware_interface.hpp create mode 100644 src/hardware_interfaces/laser_ros2_control/laser_hardware_interface.xml create mode 100644 src/hardware_interfaces/laser_ros2_control/package.xml create mode 100644 src/hardware_interfaces/laser_ros2_control/src/laser_hardware_interface.cpp create mode 100644 src/hardware_interfaces/led_ros2_control/CMakeLists.txt create mode 100644 src/hardware_interfaces/led_ros2_control/include/led_ros2_control/led_hardware_interface.hpp create mode 100644 src/hardware_interfaces/led_ros2_control/led_hardware_interface.xml create mode 100644 src/hardware_interfaces/led_ros2_control/package.xml create mode 100644 src/hardware_interfaces/led_ros2_control/src/led_hardware_interface.cpp diff --git a/src/description/ros2_control/science/science.killswitch.ros2_control.xacro b/src/description/ros2_control/science/science.killswitch.ros2_control.xacro new file mode 100644 index 0000000..822a298 --- /dev/null +++ b/src/description/ros2_control/science/science.killswitch.ros2_control.xacro @@ -0,0 +1,33 @@ + + + + + + + + + killswitch_ros2_control/KillswitchHardwareInterface + ${button_gpio_pin} + ${main_power_gpio_pin} + ${jetson_power_gpio_pin} + ${all_power_gpio_pin} + ${active_high} + + + + + + + + + + + + + + + + + + + diff --git a/src/description/ros2_control/science/science.laser.ros2_control.xacro b/src/description/ros2_control/science/science.laser.ros2_control.xacro new file mode 100644 index 0000000..f981886 --- /dev/null +++ b/src/description/ros2_control/science/science.laser.ros2_control.xacro @@ -0,0 +1,28 @@ + + + + + + + + + laser_ros2_control/LaserHardwareInterface + ${gpio_pin} + ${min_power} + ${max_power} + + + + + + + + + + + + + + + + diff --git a/src/description/ros2_control/science/science.led.ros2_control.xacro b/src/description/ros2_control/science/science.led.ros2_control.xacro new file mode 100644 index 0000000..cb96abc --- /dev/null +++ b/src/description/ros2_control/science/science.led.ros2_control.xacro @@ -0,0 +1,25 @@ + + + + + + + + + led_ros2_control/LEDHardwareInterface + ${gpio_pin} + ${default_state} + + + + + + + + + + + + + + diff --git a/src/description/urdf/athena_science.urdf.xacro b/src/description/urdf/athena_science.urdf.xacro index ca032f7..b88a75f 100644 --- a/src/description/urdf/athena_science.urdf.xacro +++ b/src/description/urdf/athena_science.urdf.xacro @@ -9,6 +9,9 @@ + + + @@ -30,7 +33,21 @@ - + + + + + + + + + + \ No newline at end of file diff --git a/src/hardware_interfaces/README.md b/src/hardware_interfaces/README.md index 2488a49..a083d58 100644 --- a/src/hardware_interfaces/README.md +++ b/src/hardware_interfaces/README.md @@ -4,8 +4,12 @@ This directory contains ROS2 Control hardware interface implementations for vari ## Available Interfaces -- `actuator_ros2_control/` - Generic actuator interface +- `killswitch_ros2_control/` - Killswitch hardware interface +- `laser_ros2_control/` - Laser hardware interface +- `led_ros2_control/` - LED hardware interface - `rmd_ros2_control/` - RMD motor controller interface +- `ros_odrive/` - ODrive motor controller interface +- `servo_ros2_control/` - Servo motor controller interface - `smc_ros2_control/` - SMC motor controller interface +- `stepper_ros2_control/` - Stepper motor controller interface - `talon_ros2_control/` - Talon motor controller interface -- `ros_odrive/` - ODrive motor controller interface diff --git a/src/hardware_interfaces/killswitch_ros2_control/CMakeLists.txt b/src/hardware_interfaces/killswitch_ros2_control/CMakeLists.txt new file mode 100644 index 0000000..e3eb609 --- /dev/null +++ b/src/hardware_interfaces/killswitch_ros2_control/CMakeLists.txt @@ -0,0 +1,69 @@ +cmake_minimum_required(VERSION 3.8) +project(killswitch_ros2_control) + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +set(THIS_PACKAGE_INCLUDE_DEPENDS + hardware_interface + pluginlib + rclcpp + rclcpp_lifecycle +) + +# find dependencies +find_package(ament_cmake REQUIRED) +find_package(ament_cmake_auto REQUIRED) +foreach(Dependency IN ITEMS ${THIS_PACKAGE_INCLUDE_DEPENDS}) + find_package(${Dependency} REQUIRED) +endforeach() + +ament_auto_find_build_dependencies() + +include_directories(include) + +add_library( + killswitch_ros2_control + SHARED + src/killswitch_hardware_interface.cpp +) + +target_compile_features(killswitch_ros2_control PUBLIC cxx_std_20) +target_include_directories(killswitch_ros2_control PUBLIC +$ +$ +) + +ament_target_dependencies( + killswitch_ros2_control PUBLIC + ${THIS_PACKAGE_INCLUDE_DEPENDS} +) + +# Export hardware plugins +pluginlib_export_plugin_description_file(hardware_interface killswitch_hardware_interface.xml) + +# INSTALL +install( + DIRECTORY include/ + DESTINATION include/killswitch_ros2_control +) + +install(TARGETS killswitch_ros2_control + EXPORT export_killswitch_ros2_control + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin +) + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + set(ament_cmake_copyright_FOUND TRUE) + set(ament_cmake_cpplint_FOUND TRUE) + ament_lint_auto_find_test_dependencies() +endif() + +ament_export_targets(export_killswitch_ros2_control) +ament_export_include_directories(include) +ament_export_dependencies(${THIS_PACKAGE_INCLUDE_DEPENDS}) +ament_package() diff --git a/src/hardware_interfaces/killswitch_ros2_control/include/killswitch_ros2_control/killswitch_hardware_interface.hpp b/src/hardware_interfaces/killswitch_ros2_control/include/killswitch_ros2_control/killswitch_hardware_interface.hpp new file mode 100644 index 0000000..421e6c2 --- /dev/null +++ b/src/hardware_interfaces/killswitch_ros2_control/include/killswitch_ros2_control/killswitch_hardware_interface.hpp @@ -0,0 +1,134 @@ +// Copyright (c) 2024 UMD Loop +// +// 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. +// + +#ifndef KILLSWITCH_ROS2_CONTROL__KILLSWITCH_HARDWARE_INTERFACE_HPP_ +#define KILLSWITCH_ROS2_CONTROL__KILLSWITCH_HARDWARE_INTERFACE_HPP_ + +#include +#include +#include + +#include "hardware_interface/handle.hpp" +#include "hardware_interface/hardware_info.hpp" +#include "hardware_interface/system_interface.hpp" +#include "hardware_interface/types/hardware_interface_return_values.hpp" +#include "rclcpp/macros.hpp" +#include "rclcpp_lifecycle/state.hpp" + +namespace killswitch_ros2_control +{ + +// GPIO utility functions (Linux sysfs) +namespace gpio_utils +{ + int setup_gpio_input(int pin); + int setup_gpio_output(int pin); + void cleanup_gpio(int pin, int fd); + bool read_gpio(int fd); + bool write_gpio(int fd, bool value); +} + +/** + * @brief Hardware interface for drive killswitch system via ros2_control + * + * This is a SystemInterface that monitors a physical killswitch button + * and controls power kill relays for main power, Jetson power, and all power. + * + * State Interfaces (read by controllers): + * - button_state: Physical button state (0.0 = not pressed, 1.0 = pressed) + * - main_power_killed: Main power relay state (0.0 = on, 1.0 = killed) + * - jetson_power_killed: Jetson power relay state (0.0 = on, 1.0 = killed) + * - all_power_killed: All power relay state (0.0 = on, 1.0 = killed) + * - is_connected: Hardware ready status + * + * Command Interfaces (written by controllers): + * - kill_main_power: Set to 1.0 to kill main power, 0.0 to restore + * - kill_jetson_power: Set to 1.0 to kill Jetson power, 0.0 to restore + * - kill_all_power: Set to 1.0 to kill all power, 0.0 to restore + * + * Hardware Parameters (from URDF): + * - button_gpio_pin: GPIO pin for killswitch button input (required) + * - main_power_gpio_pin: GPIO pin for main power relay output (required) + * - jetson_power_gpio_pin: GPIO pin for Jetson power relay output (required) + * - all_power_gpio_pin: GPIO pin for all-power relay output (required) + * - active_high: If true, HIGH = button pressed (default: true) + */ +class KillswitchHardwareInterface : public hardware_interface::SystemInterface +{ +public: + RCLCPP_SHARED_PTR_DEFINITIONS(KillswitchHardwareInterface) + + hardware_interface::CallbackReturn on_init( + const hardware_interface::HardwareInfo & info) override; + + hardware_interface::CallbackReturn on_configure( + const rclcpp_lifecycle::State & previous_state) override; + + std::vector export_state_interfaces() override; + + std::vector export_command_interfaces() override; + + hardware_interface::CallbackReturn on_activate( + const rclcpp_lifecycle::State & previous_state) override; + + hardware_interface::CallbackReturn on_deactivate( + const rclcpp_lifecycle::State & previous_state) override; + + hardware_interface::CallbackReturn on_cleanup( + const rclcpp_lifecycle::State & previous_state) override; + + hardware_interface::CallbackReturn on_shutdown( + const rclcpp_lifecycle::State & previous_state) override; + + hardware_interface::return_type read( + const rclcpp::Time & time, + const rclcpp::Duration & period) override; + + hardware_interface::return_type write( + const rclcpp::Time & time, + const rclcpp::Duration & period) override; + +private: + // Configuration parameters (GPIO pins) + int button_gpio_pin_; + int main_power_gpio_pin_; + int jetson_power_gpio_pin_; + int all_power_gpio_pin_; + bool active_high_; + + // State variables (hardware → ros2_control) + double button_state_; // Physical button: 0.0 = not pressed, 1.0 = pressed + double main_power_killed_; // 0.0 = power on, 1.0 = killed + double jetson_power_killed_; // 0.0 = power on, 1.0 = killed + double all_power_killed_; // 0.0 = power on, 1.0 = killed + double is_connected_; // Hardware ready status + + // Command variables (ros2_control → hardware) + double cmd_kill_main_power_; + double cmd_kill_jetson_power_; + double cmd_kill_all_power_; + + // GPIO file descriptors + int button_fd_; + int main_power_fd_; + int jetson_power_fd_; + int all_power_fd_; + bool hw_connected_; +}; + +} // namespace killswitch_ros2_control + +#endif // KILLSWITCH_ROS2_CONTROL__KILLSWITCH_HARDWARE_INTERFACE_HPP_ + diff --git a/src/hardware_interfaces/killswitch_ros2_control/killswitch_hardware_interface.xml b/src/hardware_interfaces/killswitch_ros2_control/killswitch_hardware_interface.xml new file mode 100644 index 0000000..9fd8e26 --- /dev/null +++ b/src/hardware_interfaces/killswitch_ros2_control/killswitch_hardware_interface.xml @@ -0,0 +1,10 @@ + + + + Drive killswitch hardware interface via ros2_control. + Monitors physical killswitch button and controls power kill relays. + + + diff --git a/src/hardware_interfaces/killswitch_ros2_control/package.xml b/src/hardware_interfaces/killswitch_ros2_control/package.xml new file mode 100644 index 0000000..37f7409 --- /dev/null +++ b/src/hardware_interfaces/killswitch_ros2_control/package.xml @@ -0,0 +1,26 @@ + + + + killswitch_ros2_control + 0.1.0 + Killswitch hardware interface for ros2_control - GPIO-based drive killswitch system + Dwarakesh Baraneetharan + Apache-2.0 + + ament_cmake + ament_cmake + + hardware_interface + pluginlib + rclcpp + rclcpp_lifecycle + + ament_lint_auto + ament_lint_common + + ament_cmake + + + ament_cmake + + diff --git a/src/hardware_interfaces/killswitch_ros2_control/src/killswitch_hardware_interface.cpp b/src/hardware_interfaces/killswitch_ros2_control/src/killswitch_hardware_interface.cpp new file mode 100644 index 0000000..dedb805 --- /dev/null +++ b/src/hardware_interfaces/killswitch_ros2_control/src/killswitch_hardware_interface.cpp @@ -0,0 +1,483 @@ +// Copyright (c) 2024 UMD Loop +// +// 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 "killswitch_ros2_control/killswitch_hardware_interface.hpp" + +#include +#include +#include + +#include "hardware_interface/types/hardware_interface_type_values.hpp" +#include "rclcpp/rclcpp.hpp" + +namespace killswitch_ros2_control +{ + +// GPIO utility functions implementation +namespace gpio_utils +{ + +int setup_gpio_output(int pin) +{ + // Export GPIO + int fd = ::open("/sys/class/gpio/export", O_WRONLY); + if (fd >= 0) { + std::string pin_str = std::to_string(pin); + ::write(fd, pin_str.c_str(), pin_str.length()); + ::close(fd); + usleep(100000); // Wait for GPIO to be exported + } + + // Set direction to output + std::stringstream direction_path; + direction_path << "/sys/class/gpio/gpio" << pin << "/direction"; + fd = ::open(direction_path.str().c_str(), O_WRONLY); + if (fd < 0) { + return -1; + } + ::write(fd, "out", 3); + ::close(fd); + + // Open value file + std::stringstream value_path; + value_path << "/sys/class/gpio/gpio" << pin << "/value"; + int value_fd = ::open(value_path.str().c_str(), O_RDWR); + + return value_fd; +} + +int setup_gpio_input(int pin) +{ + // Export GPIO + int fd = ::open("/sys/class/gpio/export", O_WRONLY); + if (fd >= 0) { + std::string pin_str = std::to_string(pin); + ::write(fd, pin_str.c_str(), pin_str.length()); + ::close(fd); + usleep(100000); // Wait for GPIO to be exported + } + + // Set direction to input + std::stringstream direction_path; + direction_path << "/sys/class/gpio/gpio" << pin << "/direction"; + fd = ::open(direction_path.str().c_str(), O_WRONLY); + if (fd < 0) { + return -1; + } + ::write(fd, "in", 2); + ::close(fd); + + // Open value file + std::stringstream value_path; + value_path << "/sys/class/gpio/gpio" << pin << "/value"; + int value_fd = ::open(value_path.str().c_str(), O_RDONLY); + + return value_fd; +} + +void cleanup_gpio(int pin, int fd) +{ + if (fd >= 0) { + ::close(fd); + } + + // Unexport GPIO + int export_fd = ::open("/sys/class/gpio/unexport", O_WRONLY); + if (export_fd >= 0) { + std::string pin_str = std::to_string(pin); + ::write(export_fd, pin_str.c_str(), pin_str.length()); + ::close(export_fd); + } +} + +bool write_gpio(int fd, bool value) +{ + if (fd < 0) return false; + + char val = value ? '1' : '0'; + lseek(fd, 0, SEEK_SET); + return (::write(fd, &val, 1) == 1); +} + +bool read_gpio(int fd) +{ + if (fd < 0) return false; + + char value; + lseek(fd, 0, SEEK_SET); + if (::read(fd, &value, 1) == 1) { + return (value == '1'); + } + return false; +} + +} // namespace gpio_utils + +// Hardware Interface Implementation + +hardware_interface::CallbackReturn KillswitchHardwareInterface::on_init( + const hardware_interface::HardwareInfo & info) +{ + if (hardware_interface::SystemInterface::on_init(info) != + hardware_interface::CallbackReturn::SUCCESS) + { + return hardware_interface::CallbackReturn::ERROR; + } + + // Initialize state variables (all power ON, button not pressed) + button_state_ = 0.0; + main_power_killed_ = 0.0; + jetson_power_killed_ = 0.0; + all_power_killed_ = 0.0; + is_connected_ = 0.0; + + // Initialize command variables (don't kill anything on startup) + cmd_kill_main_power_ = 0.0; + cmd_kill_jetson_power_ = 0.0; + cmd_kill_all_power_ = 0.0; + + // Parse hardware parameters + auto get_required_param = [&](const std::string& name) -> int { + if (!info_.hardware_parameters.count(name)) { + RCLCPP_ERROR( + rclcpp::get_logger("KillswitchHardwareInterface"), + "%s parameter is required", name.c_str()); + return -1; + } + return std::stoi(info_.hardware_parameters.at(name)); + }; + + button_gpio_pin_ = get_required_param("button_gpio_pin"); + main_power_gpio_pin_ = get_required_param("main_power_gpio_pin"); + jetson_power_gpio_pin_ = get_required_param("jetson_power_gpio_pin"); + all_power_gpio_pin_ = get_required_param("all_power_gpio_pin"); + + if (button_gpio_pin_ < 0 || main_power_gpio_pin_ < 0 || + jetson_power_gpio_pin_ < 0 || all_power_gpio_pin_ < 0) { + return hardware_interface::CallbackReturn::ERROR; + } + + // Active high/low for button (default: true) + if (info_.hardware_parameters.count("active_high")) { + active_high_ = (info_.hardware_parameters.at("active_high") == "true"); + } else { + active_high_ = true; + } + + // Initialize file descriptors + button_fd_ = -1; + main_power_fd_ = -1; + jetson_power_fd_ = -1; + all_power_fd_ = -1; + hw_connected_ = false; + + RCLCPP_INFO( + rclcpp::get_logger("KillswitchHardwareInterface"), + "Initialized killswitch system - Button: GPIO%d, Main: GPIO%d, Jetson: GPIO%d, All: GPIO%d", + button_gpio_pin_, main_power_gpio_pin_, jetson_power_gpio_pin_, all_power_gpio_pin_); + + return hardware_interface::CallbackReturn::SUCCESS; +} + +hardware_interface::CallbackReturn KillswitchHardwareInterface::on_configure( + const rclcpp_lifecycle::State & /*previous_state*/) +{ + RCLCPP_INFO( + rclcpp::get_logger("KillswitchHardwareInterface"), + "Configuring killswitch hardware..."); + + // Setup GPIO input for button + button_fd_ = gpio_utils::setup_gpio_input(button_gpio_pin_); + if (button_fd_ < 0) { + RCLCPP_ERROR( + rclcpp::get_logger("KillswitchHardwareInterface"), + "Failed to setup button GPIO input on pin %d", button_gpio_pin_); + return hardware_interface::CallbackReturn::ERROR; + } + + // Setup GPIO outputs for power relays (initially OFF = power ON) + main_power_fd_ = gpio_utils::setup_gpio_output(main_power_gpio_pin_); + if (main_power_fd_ < 0) { + RCLCPP_ERROR( + rclcpp::get_logger("KillswitchHardwareInterface"), + "Failed to setup main power GPIO output on pin %d", main_power_gpio_pin_); + return hardware_interface::CallbackReturn::ERROR; + } + gpio_utils::write_gpio(main_power_fd_, false); // Initialize to LOW (power ON) + + jetson_power_fd_ = gpio_utils::setup_gpio_output(jetson_power_gpio_pin_); + if (jetson_power_fd_ < 0) { + RCLCPP_ERROR( + rclcpp::get_logger("KillswitchHardwareInterface"), + "Failed to setup Jetson power GPIO output on pin %d", jetson_power_gpio_pin_); + return hardware_interface::CallbackReturn::ERROR; + } + gpio_utils::write_gpio(jetson_power_fd_, false); // Initialize to LOW (power ON) + + all_power_fd_ = gpio_utils::setup_gpio_output(all_power_gpio_pin_); + if (all_power_fd_ < 0) { + RCLCPP_ERROR( + rclcpp::get_logger("KillswitchHardwareInterface"), + "Failed to setup all-power GPIO output on pin %d", all_power_gpio_pin_); + return hardware_interface::CallbackReturn::ERROR; + } + gpio_utils::write_gpio(all_power_fd_, false); // Initialize to LOW (power ON) + + hw_connected_ = true; + is_connected_ = 1.0; + + RCLCPP_INFO( + rclcpp::get_logger("KillswitchHardwareInterface"), + "Successfully configured killswitch hardware"); + + return hardware_interface::CallbackReturn::SUCCESS; +} + +std::vector +KillswitchHardwareInterface::export_state_interfaces() +{ + std::vector state_interfaces; + + // Use the joint name from URDF + const std::string& name = info_.joints[0].name; + + state_interfaces.emplace_back( + hardware_interface::StateInterface(name, "button_state", &button_state_)); + state_interfaces.emplace_back( + hardware_interface::StateInterface(name, "main_power_killed", &main_power_killed_)); + state_interfaces.emplace_back( + hardware_interface::StateInterface(name, "jetson_power_killed", &jetson_power_killed_)); + state_interfaces.emplace_back( + hardware_interface::StateInterface(name, "all_power_killed", &all_power_killed_)); + state_interfaces.emplace_back( + hardware_interface::StateInterface(name, "is_connected", &is_connected_)); + + RCLCPP_INFO( + rclcpp::get_logger("KillswitchHardwareInterface"), + "Exported %zu state interfaces", state_interfaces.size()); + + return state_interfaces; +} + +std::vector +KillswitchHardwareInterface::export_command_interfaces() +{ + std::vector command_interfaces; + + // Use the joint name from URDF + const std::string& name = info_.joints[0].name; + + command_interfaces.emplace_back( + hardware_interface::CommandInterface(name, "kill_main_power", &cmd_kill_main_power_)); + command_interfaces.emplace_back( + hardware_interface::CommandInterface(name, "kill_jetson_power", &cmd_kill_jetson_power_)); + command_interfaces.emplace_back( + hardware_interface::CommandInterface(name, "kill_all_power", &cmd_kill_all_power_)); + + RCLCPP_INFO( + rclcpp::get_logger("KillswitchHardwareInterface"), + "Exported %zu command interfaces", command_interfaces.size()); + + return command_interfaces; +} + +hardware_interface::CallbackReturn KillswitchHardwareInterface::on_activate( + const rclcpp_lifecycle::State & /*previous_state*/) +{ + RCLCPP_INFO( + rclcpp::get_logger("KillswitchHardwareInterface"), + "Activating killswitch hardware..."); + + // Reset commands to safe state (don't kill anything) + cmd_kill_main_power_ = 0.0; + cmd_kill_jetson_power_ = 0.0; + cmd_kill_all_power_ = 0.0; + + return hardware_interface::CallbackReturn::SUCCESS; +} + +hardware_interface::CallbackReturn KillswitchHardwareInterface::on_deactivate( + const rclcpp_lifecycle::State & /*previous_state*/) +{ + RCLCPP_INFO( + rclcpp::get_logger("KillswitchHardwareInterface"), + "Deactivating killswitch hardware..."); + + // Note: We intentionally do NOT kill power on deactivate + // The killswitch should only respond to explicit commands + // GPIO cleanup is done in on_cleanup + + return hardware_interface::CallbackReturn::SUCCESS; +} + +hardware_interface::CallbackReturn KillswitchHardwareInterface::on_cleanup( + const rclcpp_lifecycle::State & /*previous_state*/) +{ + RCLCPP_INFO( + rclcpp::get_logger("KillswitchHardwareInterface"), + "Cleaning up killswitch hardware..."); + + // Cleanup GPIO - unexport and close file descriptors + if (button_fd_ >= 0) { + gpio_utils::cleanup_gpio(button_gpio_pin_, button_fd_); + button_fd_ = -1; + } + if (main_power_fd_ >= 0) { + gpio_utils::cleanup_gpio(main_power_gpio_pin_, main_power_fd_); + main_power_fd_ = -1; + } + if (jetson_power_fd_ >= 0) { + gpio_utils::cleanup_gpio(jetson_power_gpio_pin_, jetson_power_fd_); + jetson_power_fd_ = -1; + } + if (all_power_fd_ >= 0) { + gpio_utils::cleanup_gpio(all_power_gpio_pin_, all_power_fd_); + all_power_fd_ = -1; + } + + hw_connected_ = false; + is_connected_ = 0.0; + + RCLCPP_INFO( + rclcpp::get_logger("KillswitchHardwareInterface"), + "Killswitch hardware cleanup complete"); + + return hardware_interface::CallbackReturn::SUCCESS; +} + +hardware_interface::CallbackReturn KillswitchHardwareInterface::on_shutdown( + const rclcpp_lifecycle::State & previous_state) +{ + RCLCPP_INFO( + rclcpp::get_logger("KillswitchHardwareInterface"), + "Shutting down killswitch hardware..."); + + // Delegate to cleanup to release resources + return on_cleanup(previous_state); +} + +hardware_interface::return_type KillswitchHardwareInterface::read( + const rclcpp::Time & /*time*/, + const rclcpp::Duration & /*period*/) +{ + if (!hw_connected_) { + return hardware_interface::return_type::OK; + } + + // Read physical button state + if (button_fd_ >= 0) { + bool gpio_value = gpio_utils::read_gpio(button_fd_); + bool is_pressed = active_high_ ? gpio_value : !gpio_value; + + double previous_state = button_state_; + button_state_ = is_pressed ? 1.0 : 0.0; + + // Log state changes + if (is_pressed && previous_state < 0.5) { + RCLCPP_WARN( + rclcpp::get_logger("KillswitchHardwareInterface"), + "KILLSWITCH BUTTON PRESSED!"); + } else if (!is_pressed && previous_state > 0.5) { + RCLCPP_INFO( + rclcpp::get_logger("KillswitchHardwareInterface"), + "Killswitch button released"); + } + } + + return hardware_interface::return_type::OK; +} + +hardware_interface::return_type KillswitchHardwareInterface::write( + const rclcpp::Time & /*time*/, + const rclcpp::Duration & /*period*/) +{ + if (!hw_connected_) { + return hardware_interface::return_type::OK; + } + + // Process kill_main_power command + if (main_power_fd_ >= 0) { + bool should_kill = (cmd_kill_main_power_ > 0.5); + bool currently_killed = (main_power_killed_ > 0.5); + + if (should_kill != currently_killed) { + gpio_utils::write_gpio(main_power_fd_, should_kill); + main_power_killed_ = should_kill ? 1.0 : 0.0; + + if (should_kill) { + RCLCPP_WARN( + rclcpp::get_logger("KillswitchHardwareInterface"), + "MAIN POWER KILLED"); + } else { + RCLCPP_INFO( + rclcpp::get_logger("KillswitchHardwareInterface"), + "Main power restored"); + } + } + } + + // Process kill_jetson_power command + if (jetson_power_fd_ >= 0) { + bool should_kill = (cmd_kill_jetson_power_ > 0.5); + bool currently_killed = (jetson_power_killed_ > 0.5); + + if (should_kill != currently_killed) { + gpio_utils::write_gpio(jetson_power_fd_, should_kill); + jetson_power_killed_ = should_kill ? 1.0 : 0.0; + + if (should_kill) { + RCLCPP_WARN( + rclcpp::get_logger("KillswitchHardwareInterface"), + "JETSON POWER KILLED"); + } else { + RCLCPP_INFO( + rclcpp::get_logger("KillswitchHardwareInterface"), + "Jetson power restored"); + } + } + } + + // Process kill_all_power command + if (all_power_fd_ >= 0) { + bool should_kill = (cmd_kill_all_power_ > 0.5); + bool currently_killed = (all_power_killed_ > 0.5); + + if (should_kill != currently_killed) { + gpio_utils::write_gpio(all_power_fd_, should_kill); + all_power_killed_ = should_kill ? 1.0 : 0.0; + + if (should_kill) { + RCLCPP_ERROR( + rclcpp::get_logger("KillswitchHardwareInterface"), + "ALL POWER KILLED"); + } else { + RCLCPP_INFO( + rclcpp::get_logger("KillswitchHardwareInterface"), + "All power restored"); + } + } + } + + return hardware_interface::return_type::OK; +} + +} // namespace killswitch_ros2_control + +#include "pluginlib/class_list_macros.hpp" + +PLUGINLIB_EXPORT_CLASS( + killswitch_ros2_control::KillswitchHardwareInterface, + hardware_interface::SystemInterface) + diff --git a/src/hardware_interfaces/laser_ros2_control/CMakeLists.txt b/src/hardware_interfaces/laser_ros2_control/CMakeLists.txt new file mode 100644 index 0000000..cca55ab --- /dev/null +++ b/src/hardware_interfaces/laser_ros2_control/CMakeLists.txt @@ -0,0 +1,69 @@ +cmake_minimum_required(VERSION 3.8) +project(laser_ros2_control) + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +set(THIS_PACKAGE_INCLUDE_DEPENDS + hardware_interface + pluginlib + rclcpp + rclcpp_lifecycle +) + +# find dependencies +find_package(ament_cmake REQUIRED) +find_package(ament_cmake_auto REQUIRED) +foreach(Dependency IN ITEMS ${THIS_PACKAGE_INCLUDE_DEPENDS}) + find_package(${Dependency} REQUIRED) +endforeach() + +ament_auto_find_build_dependencies() + +include_directories(include) + +add_library( + laser_ros2_control + SHARED + src/laser_hardware_interface.cpp +) + +target_compile_features(laser_ros2_control PUBLIC cxx_std_20) +target_include_directories(laser_ros2_control PUBLIC +$ +$ +) + +ament_target_dependencies( + laser_ros2_control PUBLIC + ${THIS_PACKAGE_INCLUDE_DEPENDS} +) + +# Export hardware plugins +pluginlib_export_plugin_description_file(hardware_interface laser_hardware_interface.xml) + +# INSTALL +install( + DIRECTORY include/ + DESTINATION include/laser_ros2_control +) + +install(TARGETS laser_ros2_control + EXPORT export_laser_ros2_control + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin +) + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + set(ament_cmake_copyright_FOUND TRUE) + set(ament_cmake_cpplint_FOUND TRUE) + ament_lint_auto_find_test_dependencies() +endif() + +ament_export_targets(export_laser_ros2_control) +ament_export_include_directories(include) +ament_export_dependencies(${THIS_PACKAGE_INCLUDE_DEPENDS}) +ament_package() diff --git a/src/hardware_interfaces/laser_ros2_control/include/laser_ros2_control/laser_hardware_interface.hpp b/src/hardware_interfaces/laser_ros2_control/include/laser_ros2_control/laser_hardware_interface.hpp new file mode 100644 index 0000000..2144799 --- /dev/null +++ b/src/hardware_interfaces/laser_ros2_control/include/laser_ros2_control/laser_hardware_interface.hpp @@ -0,0 +1,118 @@ +// Copyright (c) 2024 UMD Loop +// +// 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. +// + +#ifndef LASER_ROS2_CONTROL__LASER_HARDWARE_INTERFACE_HPP_ +#define LASER_ROS2_CONTROL__LASER_HARDWARE_INTERFACE_HPP_ + +#include +#include +#include + +#include "hardware_interface/handle.hpp" +#include "hardware_interface/hardware_info.hpp" +#include "hardware_interface/system_interface.hpp" +#include "hardware_interface/types/hardware_interface_return_values.hpp" +#include "rclcpp/macros.hpp" +#include "rclcpp_lifecycle/state.hpp" + +namespace laser_ros2_control +{ + +// GPIO utility functions (Linux sysfs) +namespace gpio_utils +{ + int setup_gpio_output(int pin); + void cleanup_gpio(int pin, int fd); + bool write_gpio(int fd, bool value); +} + +/** + * @brief Hardware interface for GPIO-controlled laser via ros2_control + * + * This interface controls a spectrometry laser through GPIO pins. + * + * State Interfaces (read by controllers): + * - laser_state: 0.0 = OFF, 1.0 = ON + * - power_level: Current power level (0.0 to 1.0) + * - is_ready: Is hardware connected and ready (0.0 or 1.0) + * + * Command Interfaces (written by controllers): + * - laser_command: 0.0 = turn OFF, 1.0 = turn ON + * - power_command: Desired power level (0.0 to 1.0) + * + * Hardware Parameters (from URDF): + * - gpio_pin: GPIO pin number for laser control (required) + * - min_power: Minimum power level (default: 0.0) + * - max_power: Maximum power level (default: 1.0) + */ +class LaserHardwareInterface : public hardware_interface::SystemInterface +{ +public: + RCLCPP_SHARED_PTR_DEFINITIONS(LaserHardwareInterface) + + hardware_interface::CallbackReturn on_init( + const hardware_interface::HardwareInfo & info) override; + + hardware_interface::CallbackReturn on_configure( + const rclcpp_lifecycle::State & previous_state) override; + + std::vector export_state_interfaces() override; + + std::vector export_command_interfaces() override; + + hardware_interface::CallbackReturn on_activate( + const rclcpp_lifecycle::State & previous_state) override; + + hardware_interface::CallbackReturn on_deactivate( + const rclcpp_lifecycle::State & previous_state) override; + + hardware_interface::CallbackReturn on_cleanup( + const rclcpp_lifecycle::State & previous_state) override; + + hardware_interface::CallbackReturn on_shutdown( + const rclcpp_lifecycle::State & previous_state) override; + + hardware_interface::return_type read( + const rclcpp::Time & time, + const rclcpp::Duration & period) override; + + hardware_interface::return_type write( + const rclcpp::Time & time, + const rclcpp::Duration & period) override; + +private: + // Configuration parameters + int gpio_pin_; + double min_power_; + double max_power_; + + // State variables (hardware → ros2_control) + double laser_state_; // Current state: 0.0 = OFF, 1.0 = ON + double power_level_; // Current power level + double is_ready_; // Hardware ready status + + // Command variables (ros2_control → hardware) + double laser_command_; // Commanded state + double power_command_; // Commanded power level + + // GPIO interface + int gpio_fd_; + bool hw_connected_; +}; + +} // namespace laser_ros2_control + +#endif // LASER_ROS2_CONTROL__LASER_HARDWARE_INTERFACE_HPP_ + diff --git a/src/hardware_interfaces/laser_ros2_control/laser_hardware_interface.xml b/src/hardware_interfaces/laser_ros2_control/laser_hardware_interface.xml new file mode 100644 index 0000000..c0488a2 --- /dev/null +++ b/src/hardware_interfaces/laser_ros2_control/laser_hardware_interface.xml @@ -0,0 +1,10 @@ + + + + Hardware interface for spectrometry laser control via ros2_control. + Supports on/off control and power level via GPIO. + + + diff --git a/src/hardware_interfaces/laser_ros2_control/package.xml b/src/hardware_interfaces/laser_ros2_control/package.xml new file mode 100644 index 0000000..c3f5f95 --- /dev/null +++ b/src/hardware_interfaces/laser_ros2_control/package.xml @@ -0,0 +1,26 @@ + + + + laser_ros2_control + 0.1.0 + Laser hardware interface for ros2_control - GPIO-based spectrometry laser control + Dwarakesh Baraneetharan + Apache-2.0 + + ament_cmake + ament_cmake + + hardware_interface + pluginlib + rclcpp + rclcpp_lifecycle + + ament_lint_auto + ament_lint_common + + ament_cmake + + + ament_cmake + + diff --git a/src/hardware_interfaces/laser_ros2_control/src/laser_hardware_interface.cpp b/src/hardware_interfaces/laser_ros2_control/src/laser_hardware_interface.cpp new file mode 100644 index 0000000..15376f3 --- /dev/null +++ b/src/hardware_interfaces/laser_ros2_control/src/laser_hardware_interface.cpp @@ -0,0 +1,338 @@ +// Copyright (c) 2024 UMD Loop +// +// 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 "laser_ros2_control/laser_hardware_interface.hpp" + +#include +#include +#include +#include + +#include "hardware_interface/types/hardware_interface_type_values.hpp" +#include "rclcpp/rclcpp.hpp" + +namespace laser_ros2_control +{ + +// GPIO utility functions implementation +namespace gpio_utils +{ + +int setup_gpio_output(int pin) +{ + // Export GPIO + int fd = ::open("/sys/class/gpio/export", O_WRONLY); + if (fd >= 0) { + std::string pin_str = std::to_string(pin); + ::write(fd, pin_str.c_str(), pin_str.length()); + ::close(fd); + usleep(100000); // Wait for GPIO to be exported + } + + // Set direction to output + std::stringstream direction_path; + direction_path << "/sys/class/gpio/gpio" << pin << "/direction"; + fd = ::open(direction_path.str().c_str(), O_WRONLY); + if (fd < 0) { + return -1; + } + ::write(fd, "out", 3); + ::close(fd); + + // Open value file + std::stringstream value_path; + value_path << "/sys/class/gpio/gpio" << pin << "/value"; + int value_fd = ::open(value_path.str().c_str(), O_RDWR); + + return value_fd; +} + +void cleanup_gpio(int pin, int fd) +{ + if (fd >= 0) { + ::close(fd); + } + + // Unexport GPIO + int export_fd = ::open("/sys/class/gpio/unexport", O_WRONLY); + if (export_fd >= 0) { + std::string pin_str = std::to_string(pin); + ::write(export_fd, pin_str.c_str(), pin_str.length()); + ::close(export_fd); + } +} + +bool write_gpio(int fd, bool value) +{ + if (fd < 0) return false; + + char val = value ? '1' : '0'; + lseek(fd, 0, SEEK_SET); + return (::write(fd, &val, 1) == 1); +} + +} // namespace gpio_utils + +// Hardware Interface Implementation + +hardware_interface::CallbackReturn LaserHardwareInterface::on_init( + const hardware_interface::HardwareInfo & info) +{ + if (hardware_interface::SystemInterface::on_init(info) != + hardware_interface::CallbackReturn::SUCCESS) + { + return hardware_interface::CallbackReturn::ERROR; + } + + // Initialize state variables + laser_state_ = 0.0; // OFF + power_level_ = 0.0; + is_ready_ = 0.0; + + // Initialize command variables + laser_command_ = 0.0; // OFF + power_command_ = 0.0; + + // Parse hardware parameters + if (!info_.hardware_parameters.count("gpio_pin")) { + RCLCPP_ERROR( + rclcpp::get_logger("LaserHardwareInterface"), + "gpio_pin parameter is required"); + return hardware_interface::CallbackReturn::ERROR; + } + gpio_pin_ = std::stoi(info_.hardware_parameters.at("gpio_pin")); + + if (info_.hardware_parameters.count("min_power")) { + min_power_ = std::stod(info_.hardware_parameters.at("min_power")); + } else { + min_power_ = 0.0; + } + + if (info_.hardware_parameters.count("max_power")) { + max_power_ = std::stod(info_.hardware_parameters.at("max_power")); + } else { + max_power_ = 1.0; + } + + gpio_fd_ = -1; + hw_connected_ = false; + + RCLCPP_INFO( + rclcpp::get_logger("LaserHardwareInterface"), + "Initialized laser on GPIO pin %d", gpio_pin_); + + return hardware_interface::CallbackReturn::SUCCESS; +} + +hardware_interface::CallbackReturn LaserHardwareInterface::on_configure( + const rclcpp_lifecycle::State & /*previous_state*/) +{ + RCLCPP_INFO( + rclcpp::get_logger("LaserHardwareInterface"), + "Configuring laser hardware..."); + + // Setup GPIO output + gpio_fd_ = gpio_utils::setup_gpio_output(gpio_pin_); + if (gpio_fd_ < 0) { + RCLCPP_ERROR( + rclcpp::get_logger("LaserHardwareInterface"), + "Failed to setup GPIO output on pin %d", gpio_pin_); + return hardware_interface::CallbackReturn::ERROR; + } + + hw_connected_ = true; + is_ready_ = 1.0; + + RCLCPP_INFO( + rclcpp::get_logger("LaserHardwareInterface"), + "Successfully configured laser hardware"); + + return hardware_interface::CallbackReturn::SUCCESS; +} + +std::vector +LaserHardwareInterface::export_state_interfaces() +{ + std::vector state_interfaces; + + // Use the joint name from URDF + const std::string& name = info_.joints[0].name; + + // Laser state (ON/OFF) + state_interfaces.emplace_back( + hardware_interface::StateInterface(name, "laser_state", &laser_state_)); + + // Power level + state_interfaces.emplace_back( + hardware_interface::StateInterface(name, "power_level", &power_level_)); + + // Hardware ready status + state_interfaces.emplace_back( + hardware_interface::StateInterface(name, "is_ready", &is_ready_)); + + RCLCPP_INFO( + rclcpp::get_logger("LaserHardwareInterface"), + "Exported %zu state interfaces", state_interfaces.size()); + + return state_interfaces; +} + +std::vector +LaserHardwareInterface::export_command_interfaces() +{ + std::vector command_interfaces; + + // Use the joint name from URDF + const std::string& name = info_.joints[0].name; + + // Laser command (ON/OFF) + command_interfaces.emplace_back( + hardware_interface::CommandInterface(name, "laser_command", &laser_command_)); + + // Power command + command_interfaces.emplace_back( + hardware_interface::CommandInterface(name, "power_command", &power_command_)); + + RCLCPP_INFO( + rclcpp::get_logger("LaserHardwareInterface"), + "Exported %zu command interfaces", command_interfaces.size()); + + return command_interfaces; +} + +hardware_interface::CallbackReturn LaserHardwareInterface::on_activate( + const rclcpp_lifecycle::State & /*previous_state*/) +{ + RCLCPP_INFO( + rclcpp::get_logger("LaserHardwareInterface"), + "Activating laser hardware..."); + + return hardware_interface::CallbackReturn::SUCCESS; +} + +hardware_interface::CallbackReturn LaserHardwareInterface::on_deactivate( + const rclcpp_lifecycle::State & /*previous_state*/) +{ + RCLCPP_INFO( + rclcpp::get_logger("LaserHardwareInterface"), + "Deactivating laser hardware..."); + + // Safety: Turn laser OFF on deactivation + if (hw_connected_ && gpio_fd_ >= 0) { + gpio_utils::write_gpio(gpio_fd_, false); + laser_state_ = 0.0; + power_level_ = 0.0; + + RCLCPP_INFO( + rclcpp::get_logger("LaserHardwareInterface"), + "Laser turned OFF (safety)"); + } + + return hardware_interface::CallbackReturn::SUCCESS; +} + +hardware_interface::CallbackReturn LaserHardwareInterface::on_cleanup( + const rclcpp_lifecycle::State & /*previous_state*/) +{ + RCLCPP_INFO( + rclcpp::get_logger("LaserHardwareInterface"), + "Cleaning up laser hardware..."); + + // Ensure laser is OFF before cleanup + if (gpio_fd_ >= 0) { + gpio_utils::write_gpio(gpio_fd_, false); + } + + // Cleanup GPIO - unexport and close file descriptor + if (gpio_fd_ >= 0) { + gpio_utils::cleanup_gpio(gpio_pin_, gpio_fd_); + gpio_fd_ = -1; + } + + hw_connected_ = false; + is_ready_ = 0.0; + + RCLCPP_INFO( + rclcpp::get_logger("LaserHardwareInterface"), + "Laser hardware cleanup complete"); + + return hardware_interface::CallbackReturn::SUCCESS; +} + +hardware_interface::CallbackReturn LaserHardwareInterface::on_shutdown( + const rclcpp_lifecycle::State & previous_state) +{ + RCLCPP_INFO( + rclcpp::get_logger("LaserHardwareInterface"), + "Shutting down laser hardware..."); + + // Delegate to cleanup to release resources + return on_cleanup(previous_state); +} + +hardware_interface::return_type LaserHardwareInterface::read( + const rclcpp::Time & /*time*/, + const rclcpp::Duration & /*period*/) +{ + // Laser state is known from commands, no need to read from GPIO + return hardware_interface::return_type::OK; +} + +hardware_interface::return_type LaserHardwareInterface::write( + const rclcpp::Time & /*time*/, + const rclcpp::Duration & /*period*/) +{ + if (!hw_connected_ || gpio_fd_ < 0) { + return hardware_interface::return_type::OK; + } + + // Check if laser command changed + bool commanded_on = (laser_command_ > 0.5); + bool currently_on = (laser_state_ > 0.5); + + if (commanded_on != currently_on) { + if (gpio_utils::write_gpio(gpio_fd_, commanded_on)) { + laser_state_ = commanded_on ? 1.0 : 0.0; + + RCLCPP_INFO( + rclcpp::get_logger("LaserHardwareInterface"), + "Laser turned %s", commanded_on ? "ON" : "OFF"); + } + } + + // Update power level (clamped to min/max) + double commanded_power = power_command_; + if (commanded_power < min_power_) commanded_power = min_power_; + if (commanded_power > max_power_) commanded_power = max_power_; + + if (std::abs(commanded_power - power_level_) > 0.01) { + power_level_ = commanded_power; + + RCLCPP_DEBUG( + rclcpp::get_logger("LaserHardwareInterface"), + "Power level set to %.2f", power_level_); + } + + return hardware_interface::return_type::OK; +} + +} // namespace laser_ros2_control + +#include "pluginlib/class_list_macros.hpp" + +PLUGINLIB_EXPORT_CLASS( + laser_ros2_control::LaserHardwareInterface, + hardware_interface::SystemInterface) + diff --git a/src/hardware_interfaces/led_ros2_control/CMakeLists.txt b/src/hardware_interfaces/led_ros2_control/CMakeLists.txt new file mode 100644 index 0000000..0e3b4e4 --- /dev/null +++ b/src/hardware_interfaces/led_ros2_control/CMakeLists.txt @@ -0,0 +1,69 @@ +cmake_minimum_required(VERSION 3.8) +project(led_ros2_control) + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +set(THIS_PACKAGE_INCLUDE_DEPENDS + hardware_interface + pluginlib + rclcpp + rclcpp_lifecycle +) + +# find dependencies +find_package(ament_cmake REQUIRED) +find_package(ament_cmake_auto REQUIRED) +foreach(Dependency IN ITEMS ${THIS_PACKAGE_INCLUDE_DEPENDS}) + find_package(${Dependency} REQUIRED) +endforeach() + +ament_auto_find_build_dependencies() + +include_directories(include) + +add_library( + led_ros2_control + SHARED + src/led_hardware_interface.cpp +) + +target_compile_features(led_ros2_control PUBLIC cxx_std_20) +target_include_directories(led_ros2_control PUBLIC +$ +$ +) + +ament_target_dependencies( + led_ros2_control PUBLIC + ${THIS_PACKAGE_INCLUDE_DEPENDS} +) + +# Export hardware plugins +pluginlib_export_plugin_description_file(hardware_interface led_hardware_interface.xml) + +# INSTALL +install( + DIRECTORY include/ + DESTINATION include/led_ros2_control +) + +install(TARGETS led_ros2_control + EXPORT export_led_ros2_control + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin +) + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + set(ament_cmake_copyright_FOUND TRUE) + set(ament_cmake_cpplint_FOUND TRUE) + ament_lint_auto_find_test_dependencies() +endif() + +ament_export_targets(export_led_ros2_control) +ament_export_include_directories(include) +ament_export_dependencies(${THIS_PACKAGE_INCLUDE_DEPENDS}) +ament_package() diff --git a/src/hardware_interfaces/led_ros2_control/include/led_ros2_control/led_hardware_interface.hpp b/src/hardware_interfaces/led_ros2_control/include/led_ros2_control/led_hardware_interface.hpp new file mode 100644 index 0000000..7919ed5 --- /dev/null +++ b/src/hardware_interfaces/led_ros2_control/include/led_ros2_control/led_hardware_interface.hpp @@ -0,0 +1,112 @@ +// Copyright (c) 2024 UMD Loop +// +// 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. +// + +#ifndef LED_ROS2_CONTROL__LED_HARDWARE_INTERFACE_HPP_ +#define LED_ROS2_CONTROL__LED_HARDWARE_INTERFACE_HPP_ + +#include +#include +#include + +#include "hardware_interface/handle.hpp" +#include "hardware_interface/hardware_info.hpp" +#include "hardware_interface/system_interface.hpp" +#include "hardware_interface/types/hardware_interface_return_values.hpp" +#include "rclcpp/macros.hpp" +#include "rclcpp_lifecycle/state.hpp" + +namespace led_ros2_control +{ + +// GPIO utility functions (Linux sysfs) +namespace gpio_utils +{ + int setup_gpio_output(int pin); + void cleanup_gpio(int pin, int fd); + bool write_gpio(int fd, bool value); +} + +/** + * @brief Hardware interface for LED control via ros2_control + * + * This is a SystemInterface for controlling status LEDs through GPIO. + * + * State Interfaces (read by controllers): + * - led_state: Current LED state (0.0 = OFF, 1.0 = ON) + * - is_connected: Is hardware connected and ready (0.0 or 1.0) + * + * Command Interfaces (written by controllers): + * - led_command: LED command (0.0 = turn OFF, 1.0 = turn ON) + * + * Hardware Parameters (from URDF): + * - gpio_pin: GPIO pin number for LED output (required) + * - default_state: "on" or "off" (default: "off") + */ +class LEDHardwareInterface : public hardware_interface::SystemInterface +{ +public: + RCLCPP_SHARED_PTR_DEFINITIONS(LEDHardwareInterface) + + hardware_interface::CallbackReturn on_init( + const hardware_interface::HardwareInfo & info) override; + + hardware_interface::CallbackReturn on_configure( + const rclcpp_lifecycle::State & previous_state) override; + + std::vector export_state_interfaces() override; + + std::vector export_command_interfaces() override; + + hardware_interface::CallbackReturn on_activate( + const rclcpp_lifecycle::State & previous_state) override; + + hardware_interface::CallbackReturn on_deactivate( + const rclcpp_lifecycle::State & previous_state) override; + + hardware_interface::CallbackReturn on_cleanup( + const rclcpp_lifecycle::State & previous_state) override; + + hardware_interface::CallbackReturn on_shutdown( + const rclcpp_lifecycle::State & previous_state) override; + + hardware_interface::return_type read( + const rclcpp::Time & time, + const rclcpp::Duration & period) override; + + hardware_interface::return_type write( + const rclcpp::Time & time, + const rclcpp::Duration & period) override; + +private: + // Configuration parameters + int gpio_pin_; + bool default_state_; + + // State variables (hardware → ros2_control) + double led_state_; // Current state: 0.0 = OFF, 1.0 = ON + double is_connected_; // Hardware ready status + + // Command variables (ros2_control → hardware) + double led_command_; // Commanded state + + // Hardware interface + int gpio_fd_; + bool hw_connected_; +}; + +} // namespace led_ros2_control + +#endif // LED_ROS2_CONTROL__LED_HARDWARE_INTERFACE_HPP_ + diff --git a/src/hardware_interfaces/led_ros2_control/led_hardware_interface.xml b/src/hardware_interfaces/led_ros2_control/led_hardware_interface.xml new file mode 100644 index 0000000..eac679c --- /dev/null +++ b/src/hardware_interfaces/led_ros2_control/led_hardware_interface.xml @@ -0,0 +1,10 @@ + + + + LED status indicator hardware interface via ros2_control. + GPIO output for controlling status LEDs. + + + diff --git a/src/hardware_interfaces/led_ros2_control/package.xml b/src/hardware_interfaces/led_ros2_control/package.xml new file mode 100644 index 0000000..10258b2 --- /dev/null +++ b/src/hardware_interfaces/led_ros2_control/package.xml @@ -0,0 +1,26 @@ + + + + led_ros2_control + 0.1.0 + LED hardware interface for ros2_control - GPIO-based status LED control + Dwarakesh Baraneetharan + Apache-2.0 + + ament_cmake + ament_cmake + + hardware_interface + pluginlib + rclcpp + rclcpp_lifecycle + + ament_lint_auto + ament_lint_common + + ament_cmake + + + ament_cmake + + diff --git a/src/hardware_interfaces/led_ros2_control/src/led_hardware_interface.cpp b/src/hardware_interfaces/led_ros2_control/src/led_hardware_interface.cpp new file mode 100644 index 0000000..7413fe6 --- /dev/null +++ b/src/hardware_interfaces/led_ros2_control/src/led_hardware_interface.cpp @@ -0,0 +1,312 @@ +// Copyright (c) 2024 UMD Loop +// +// 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 "led_ros2_control/led_hardware_interface.hpp" + +#include +#include +#include + +#include "hardware_interface/types/hardware_interface_type_values.hpp" +#include "rclcpp/rclcpp.hpp" + +namespace led_ros2_control +{ + +// GPIO utility functions implementation +namespace gpio_utils +{ + +int setup_gpio_output(int pin) +{ + // Export GPIO + int fd = ::open("/sys/class/gpio/export", O_WRONLY); + if (fd >= 0) { + std::string pin_str = std::to_string(pin); + ::write(fd, pin_str.c_str(), pin_str.length()); + ::close(fd); + usleep(100000); // Wait for GPIO to be exported + } + + // Set direction to output + std::stringstream direction_path; + direction_path << "/sys/class/gpio/gpio" << pin << "/direction"; + fd = ::open(direction_path.str().c_str(), O_WRONLY); + if (fd < 0) { + return -1; + } + ::write(fd, "out", 3); + ::close(fd); + + // Open value file + std::stringstream value_path; + value_path << "/sys/class/gpio/gpio" << pin << "/value"; + int value_fd = ::open(value_path.str().c_str(), O_RDWR); + + return value_fd; +} + +void cleanup_gpio(int pin, int fd) +{ + if (fd >= 0) { + ::close(fd); + } + + // Unexport GPIO + int export_fd = ::open("/sys/class/gpio/unexport", O_WRONLY); + if (export_fd >= 0) { + std::string pin_str = std::to_string(pin); + ::write(export_fd, pin_str.c_str(), pin_str.length()); + ::close(export_fd); + } +} + +bool write_gpio(int fd, bool value) +{ + if (fd < 0) return false; + + char val = value ? '1' : '0'; + lseek(fd, 0, SEEK_SET); + return (::write(fd, &val, 1) == 1); +} + +} // namespace gpio_utils + +// Hardware Interface Implementation + +hardware_interface::CallbackReturn LEDHardwareInterface::on_init( + const hardware_interface::HardwareInfo & info) +{ + if (hardware_interface::SystemInterface::on_init(info) != + hardware_interface::CallbackReturn::SUCCESS) + { + return hardware_interface::CallbackReturn::ERROR; + } + + // Parse hardware parameters + if (!info_.hardware_parameters.count("gpio_pin")) { + RCLCPP_ERROR( + rclcpp::get_logger("LEDHardwareInterface"), + "gpio_pin parameter is required"); + return hardware_interface::CallbackReturn::ERROR; + } + gpio_pin_ = std::stoi(info_.hardware_parameters.at("gpio_pin")); + + // Default state + if (info_.hardware_parameters.count("default_state")) { + default_state_ = (info_.hardware_parameters.at("default_state") == "on"); + } else { + default_state_ = false; // OFF by default + } + + // Initialize state variables + led_state_ = default_state_ ? 1.0 : 0.0; + is_connected_ = 0.0; + + // Initialize command variables + led_command_ = default_state_ ? 1.0 : 0.0; + + gpio_fd_ = -1; + hw_connected_ = false; + + RCLCPP_INFO( + rclcpp::get_logger("LEDHardwareInterface"), + "Initialized LED on GPIO pin %d (default: %s)", + gpio_pin_, default_state_ ? "ON" : "OFF"); + + return hardware_interface::CallbackReturn::SUCCESS; +} + +hardware_interface::CallbackReturn LEDHardwareInterface::on_configure( + const rclcpp_lifecycle::State & /*previous_state*/) +{ + RCLCPP_INFO( + rclcpp::get_logger("LEDHardwareInterface"), + "Configuring LED hardware..."); + + // Setup GPIO output + gpio_fd_ = gpio_utils::setup_gpio_output(gpio_pin_); + if (gpio_fd_ < 0) { + RCLCPP_ERROR( + rclcpp::get_logger("LEDHardwareInterface"), + "Failed to setup GPIO output on pin %d", gpio_pin_); + return hardware_interface::CallbackReturn::ERROR; + } + + hw_connected_ = true; + is_connected_ = 1.0; + + // Set initial state + gpio_utils::write_gpio(gpio_fd_, default_state_); + + RCLCPP_INFO( + rclcpp::get_logger("LEDHardwareInterface"), + "Successfully configured LED hardware"); + + return hardware_interface::CallbackReturn::SUCCESS; +} + +std::vector +LEDHardwareInterface::export_state_interfaces() +{ + std::vector state_interfaces; + + // Use the joint name from URDF + const std::string& name = info_.joints[0].name; + + // LED state + state_interfaces.emplace_back( + hardware_interface::StateInterface(name, "led_state", &led_state_)); + + // Connection status + state_interfaces.emplace_back( + hardware_interface::StateInterface(name, "is_connected", &is_connected_)); + + RCLCPP_INFO( + rclcpp::get_logger("LEDHardwareInterface"), + "Exported %zu state interfaces", state_interfaces.size()); + + return state_interfaces; +} + +std::vector +LEDHardwareInterface::export_command_interfaces() +{ + std::vector command_interfaces; + + // Use the joint name from URDF + const std::string& name = info_.joints[0].name; + + // LED command + command_interfaces.emplace_back( + hardware_interface::CommandInterface(name, "led_command", &led_command_)); + + RCLCPP_INFO( + rclcpp::get_logger("LEDHardwareInterface"), + "Exported %zu command interfaces", command_interfaces.size()); + + return command_interfaces; +} + +hardware_interface::CallbackReturn LEDHardwareInterface::on_activate( + const rclcpp_lifecycle::State & /*previous_state*/) +{ + RCLCPP_INFO( + rclcpp::get_logger("LEDHardwareInterface"), + "Activating LED hardware..."); + + return hardware_interface::CallbackReturn::SUCCESS; +} + +hardware_interface::CallbackReturn LEDHardwareInterface::on_deactivate( + const rclcpp_lifecycle::State & /*previous_state*/) +{ + RCLCPP_INFO( + rclcpp::get_logger("LEDHardwareInterface"), + "Deactivating LED hardware..."); + + // Turn LED off on deactivation (safety) + if (hw_connected_ && gpio_fd_ >= 0) { + gpio_utils::write_gpio(gpio_fd_, false); + led_state_ = 0.0; + + RCLCPP_INFO( + rclcpp::get_logger("LEDHardwareInterface"), + "LED turned OFF (deactivation)"); + } + + return hardware_interface::CallbackReturn::SUCCESS; +} + +hardware_interface::CallbackReturn LEDHardwareInterface::on_cleanup( + const rclcpp_lifecycle::State & /*previous_state*/) +{ + RCLCPP_INFO( + rclcpp::get_logger("LEDHardwareInterface"), + "Cleaning up LED hardware..."); + + // Ensure LED is OFF before cleanup + if (gpio_fd_ >= 0) { + gpio_utils::write_gpio(gpio_fd_, false); + } + + // Cleanup GPIO - unexport and close file descriptor + if (gpio_fd_ >= 0) { + gpio_utils::cleanup_gpio(gpio_pin_, gpio_fd_); + gpio_fd_ = -1; + } + + hw_connected_ = false; + is_connected_ = 0.0; + + RCLCPP_INFO( + rclcpp::get_logger("LEDHardwareInterface"), + "LED hardware cleanup complete"); + + return hardware_interface::CallbackReturn::SUCCESS; +} + +hardware_interface::CallbackReturn LEDHardwareInterface::on_shutdown( + const rclcpp_lifecycle::State & previous_state) +{ + RCLCPP_INFO( + rclcpp::get_logger("LEDHardwareInterface"), + "Shutting down LED hardware..."); + + // Delegate to cleanup to release resources + return on_cleanup(previous_state); +} + +hardware_interface::return_type LEDHardwareInterface::read( + const rclcpp::Time & /*time*/, + const rclcpp::Duration & /*period*/) +{ + // LED state is known from commands, no need to read from GPIO + return hardware_interface::return_type::OK; +} + +hardware_interface::return_type LEDHardwareInterface::write( + const rclcpp::Time & /*time*/, + const rclcpp::Duration & /*period*/) +{ + if (!hw_connected_ || gpio_fd_ < 0) { + return hardware_interface::return_type::OK; + } + + // Check if LED command changed + bool commanded_on = (led_command_ > 0.5); + bool currently_on = (led_state_ > 0.5); + + if (commanded_on != currently_on) { + if (gpio_utils::write_gpio(gpio_fd_, commanded_on)) { + led_state_ = commanded_on ? 1.0 : 0.0; + + RCLCPP_DEBUG( + rclcpp::get_logger("LEDHardwareInterface"), + "LED turned %s", commanded_on ? "ON" : "OFF"); + } + } + + return hardware_interface::return_type::OK; +} + +} // namespace led_ros2_control + +#include "pluginlib/class_list_macros.hpp" + +PLUGINLIB_EXPORT_CLASS( + led_ros2_control::LEDHardwareInterface, + hardware_interface::SystemInterface) + diff --git a/src/subsystems/science/science_bringup/config/athena_science_controllers.yaml b/src/subsystems/science/science_bringup/config/athena_science_controllers.yaml index af691e7..f355cd9 100644 --- a/src/subsystems/science/science_bringup/config/athena_science_controllers.yaml +++ b/src/subsystems/science/science_bringup/config/athena_science_controllers.yaml @@ -36,6 +36,27 @@ controller_manager: science_controller: type: science_controllers/ScienceManual + # Laser Controllers + laser_onoff_controller: + type: forward_command_controller/ForwardCommandController + + laser_power_controller: + type: forward_command_controller/ForwardCommandController + + # Killswitch Controllers (one per relay) + killswitch_main_controller: + type: forward_command_controller/ForwardCommandController + + killswitch_jetson_controller: + type: forward_command_controller/ForwardCommandController + + killswitch_all_controller: + type: forward_command_controller/ForwardCommandController + + # LED Controller + led_controller: + type: forward_command_controller/ForwardCommandController + joint_group_position_controller: ros__parameters: joints: @@ -85,4 +106,43 @@ science_controller: cap: "cap" interface_name: position command_interfaces: [position, velocity] - state_interfaces: [position, velocity] \ No newline at end of file + state_interfaces: [position, velocity] + +# Laser Controller Configurations +laser_onoff_controller: + ros__parameters: + joints: + - spectrometry_laser + interface_name: laser_command + +laser_power_controller: + ros__parameters: + joints: + - spectrometry_laser + interface_name: power_command + +# Killswitch Controller Configurations +killswitch_main_controller: + ros__parameters: + joints: + - drive_killswitch + interface_name: kill_main_power + +killswitch_jetson_controller: + ros__parameters: + joints: + - drive_killswitch + interface_name: kill_jetson_power + +killswitch_all_controller: + ros__parameters: + joints: + - drive_killswitch + interface_name: kill_all_power + +# LED Controller Configuration +led_controller: + ros__parameters: + joints: + - status_led + interface_name: led_command \ No newline at end of file