diff --git a/Assignment.md b/Assignment.md new file mode 100644 index 0000000..35b11b3 --- /dev/null +++ b/Assignment.md @@ -0,0 +1,33 @@ +# Inverted Pendulum +#### Implementation of control laws on simulated inverted pendulum on ros2 + +![Output sample](illustrations/screen-capture.gif) + +## Exercises +![Output sample](illustrations/diagram.gif) +#### 1) Single Inverted Pendulum +- **Interface** - Crete a new node which subscribes to state feedback and publishes to torque input. Give torque and check if the pendulum is behaving appropriately (You can add this node to the launch file later) +- **Balance** [Initial state - near upright] Write a controller to balance the inverted pendulum with initial state near upright position and not exactly upright +- **Swing-up** [Initial state - downward point at stable equilibrium] Write a controller to first swing up the pendulum, then balance on top. + +#### 2) Double Inverted Pendulum +- Create a new pacakge with dynamics of an double inverted pendulum + +## How-to +1. Clone this repository in /src folder of your ros2 workspace +2. Build and source the workspace. Navigate to your workspace directory and run +``` +colcon build --symlink-install +source install/setup.bash +``` +3. You can now launch the simulation +``` +ros2 launch single_inverted single_inverted_pendulum.launch.py +``` +4. Create a seperate 'controller' node which uses feedback to determine the torque value to accomplish tasks + +5. Change the initial states by varying the values theta0 and theta_dot0 manually or by un-commenting relevant sections for random initializations +6. Custom messages definitions are used for input and feedback + + + diff --git a/README.md b/README.md index 35b11b3..51813dd 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,35 @@ -# Inverted Pendulum -#### Implementation of control laws on simulated inverted pendulum on ros2 - -![Output sample](illustrations/screen-capture.gif) - -## Exercises -![Output sample](illustrations/diagram.gif) -#### 1) Single Inverted Pendulum -- **Interface** - Crete a new node which subscribes to state feedback and publishes to torque input. Give torque and check if the pendulum is behaving appropriately (You can add this node to the launch file later) -- **Balance** [Initial state - near upright] Write a controller to balance the inverted pendulum with initial state near upright position and not exactly upright -- **Swing-up** [Initial state - downward point at stable equilibrium] Write a controller to first swing up the pendulum, then balance on top. - -#### 2) Double Inverted Pendulum -- Create a new pacakge with dynamics of an double inverted pendulum - -## How-to -1. Clone this repository in /src folder of your ros2 workspace -2. Build and source the workspace. Navigate to your workspace directory and run -``` -colcon build --symlink-install -source install/setup.bash -``` -3. You can now launch the simulation -``` -ros2 launch single_inverted single_inverted_pendulum.launch.py -``` -4. Create a seperate 'controller' node which uses feedback to determine the torque value to accomplish tasks - -5. Change the initial states by varying the values theta0 and theta_dot0 manually or by un-commenting relevant sections for random initializations -6. Custom messages definitions are used for input and feedback +# Inverted Pendulum Problems +This repository contains solutions to the inverted pendulum problems.Each scenario includes a video demonstration and a corresponding performance graph. + +# Single Pendulum + +## 1. Balance + +### Video Demonstration + + + +### Performance Graph +![Graph: Balance](https://github.com/Atharav1805/Inverted-pendulum-problems-Atharav/assets/168408064/cafd3725-003c-4873-a124-6810d496d66b) + +## 2. Swing Up + +### Video Demonstration + + + + +### Performance Graph +![Graph: Swing Up](https://github.com/Atharav1805/Inverted-pendulum-problems-Atharav/assets/168408064/24006527-40cf-475a-bc8a-4237c8220786) + + +# Double Pendulum + +[double inverted pendulum.webm](https://github.com/Atharav1805/Inverted-pendulum-problems-Atharav/assets/168408064/1aea4261-4329-4b0c-b2e6-fa9eaaf0254e) diff --git a/controller_node.py b/controller_node.py new file mode 100644 index 0000000..e33ea44 --- /dev/null +++ b/controller_node.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 + +import numpy as np +import rclpy +from rclpy.node import Node +from custom_msgs.msg import States +from custom_msgs.msg import TorqueInput +import matplotlib.pyplot as plt +import time + + +class ControllerNode(Node): + + def __init__(self): + super().__init__('controller_node') + self.controller_node_subs = self.create_subscription(States, "/state_feedback", self.pose_callback, 10) # create a subscriber with the name controller_node_subs that subscribes to the state_feedback topic + self.controller_node_pub = self.create_publisher(TorqueInput, "/torque_input", 10) # create a publisher with the name controller_node_pub that publishes to the torque_input topic + #self.timer = self.create_timer(0.02, self.send_torque_info) # create a timer that calls the send_torque_info function every 0.01 seconds + #Set the state variables + self.theta = np.pi - (np.random.rand() - 0.5) / 2 + self.theta = (self.theta + np.pi)%(2*np.pi) - np.pi + self.theta_dot = 0 + self.torque = 0.0 + self.target_angle = np.pi + + # Create a list to store the theta and time values + self.theta_values = [] + self.time_values = [] + + self.node_start_time = time.time() + self.prev_time = time.time() + self.previous_error = self.target_angle - self.theta + self.integral = 0.0 + + self.get_logger().info("Controller node has been started") + + + def pose_callback(self, msg: States): + #self.get_logger().info("Received state: " + str(msg.theta) + ", " + str(msg.theta_dot)) + self.theta = msg.theta + self.theta_dot = msg.theta_dot + + + + if self.theta < 0: + error = -self.target_angle - self.theta + if self.theta >= 0: + error = self.target_angle - self.theta + + self.PID(error) + self.theta_values.append(self.theta) # append the theta value to the list + self.time_values.append(time.time() - self.node_start_time) # append the time value to the list + + + def send_torque_info(self): # create a function that sends the torque value to the simulation + + self.torque = max(-5.0, min(5.0, self.torque)) # limit the torque value to be between -5 and 5 + + #Publish the torque value + msg = TorqueInput() # create a TorqueInput message + msg.torque_value = self.torque + self.controller_node_pub.publish(msg) # publish the torque value to the topic + self.get_logger().info("Sent torque: " + str(self.torque)) # log the torque value + + + def PID(self,error): + + # Set the PID gains + Kp = 50.0 + Ki = 0.001 + Kd = 6.0 + + # Compute the time difference + dt = time.time() - self.prev_time + self.prev_time = time.time() # update the previous time + + self.integral += error * dt + derivative = (error - self.previous_error) / dt + self.previous_error = error + + p = Kp * error + i = Ki * self.integral + d = Kd * derivative + + self.torque = p + i + d # calculate the torque value + self.send_torque_info() + + + def plot_graph(self): + + plt.plot(self.time_values, self.theta_values) + plt.xlabel("Time (s)") + plt.ylabel("Theta (rad)") + plt.title("Theta vs Time") + plt.grid(True) + plt.show() + + + +def main(args=None): + rclpy.init(args=args) + node = ControllerNode() + + try: + + rclpy.spin(node) + + except KeyboardInterrupt: + + node.get_logger().info("Keyboard Interrupt detected...shutting the node down") + node.plot_graph() + pass + + rclpy.shutdown() + + + + \ No newline at end of file diff --git a/controller_node_swing_up.py b/controller_node_swing_up.py new file mode 100644 index 0000000..97c685b --- /dev/null +++ b/controller_node_swing_up.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 + +import numpy as np +import rclpy +from rclpy.node import Node +from custom_msgs.msg import States +from custom_msgs.msg import TorqueInput +import matplotlib.pyplot as plt +import time + + +class ControllerNode(Node): + + def __init__(self): + super().__init__('controller_node') + self.controller_node_subs = self.create_subscription(States, "/state_feedback", self.pose_callback, 10) # create a subscriber with the name controller_node_subs that subscribes to the state_feedback topic + self.controller_node_pub = self.create_publisher(TorqueInput, "/torque_input", 10) # create a publisher with the name controller_node_pub that publishes to the torque_input topic + #self.timer = self.create_timer(0.02, self.send_torque_info) # create a timer that calls the send_torque_info function every 0.01 seconds + #Set the state variables + self.theta = 0 - (np.random.rand() - 0.5) / 2 + self.theta = (self.theta + np.pi)%(2*np.pi) - np.pi + self.theta_dot = 0 + self.torque = 0.0 + self.target_angle = np.pi + + # Create a list to store the theta and time values + self.theta_values = [] + self.time_values = [] + + self.start_time = time.time() + self.prev_time = self.start_time + self.previous_error = self.target_angle - self.theta + self.integral = 0.0 + + self.previous_theta = self.theta + self.previous_torque = 5.0 + + self.swing_up_complete = False + + + self.get_logger().info("Controller node has been started") + + + + + + def pose_callback(self, msg: States): + #self.get_logger().info("Received state: " + str(msg.theta) + ", " + str(msg.theta_dot)) + self.theta = msg.theta + self.theta_dot = msg.theta_dot + + if self.swing_up_complete == False: + self.swing_up() + + if abs(self.theta) >= np.pi -0.4 : + self.swing_up_complete = True + self.get_logger().info("Swing up complete, now balancing the pendulum...") + + if self.swing_up_complete == True: + self.PID_balance() + + self.theta_values.append(self.theta) # append the theta value to the list + self.time_values.append(time.time() - self.start_time) # append the time value to the list + + + def swing_up(self): + + if abs(self.theta - self.previous_theta) >= 0.6: + self.torque = 0.0 + + elif abs(self.theta) < np.pi / 1.5: + if abs(self.theta - self.previous_theta) < 0.05: + self.previous_torque *= -1 + self.torque = self.previous_torque + #else: + #self.torque = self.previous_torque + + self.send_torque_info() + + + def send_torque_info(self): # create a function that sends the torque value to the simulation + + self.torque = max(-5.0, min(5.0, self.torque)) # limit the torque value to be between -5 and 5 + + #Publish the torque value + msg = TorqueInput() # create a TorqueInput message + msg.torque_value = self.torque + self.controller_node_pub.publish(msg) # publish the torque value to the topic + self.get_logger().info("Sent torque: " + str(self.torque)) # log the torque value + + + def PID_balance(self): + + # Set the PID gains + Kp = 50.0 + Ki = 0.001 + Kd = 6.0 + + if self.theta < 0: + error = -self.target_angle - self.theta + if self.theta >= 0: + error = self.target_angle - self.theta + + # Compute the time difference + dt = time.time() - self.prev_time + self.prev_time = time.time() # update the previous time + + self.integral += error * dt + derivative = (error - self.previous_error) / dt + self.previous_error = error + + p = Kp * error + i = Ki * self.integral + d = Kd * derivative + + self.torque = p + i + d + self.send_torque_info() + + + def plot_graph(self): + + plt.plot(self.time_values, self.theta_values) + plt.xlabel("Time (s)") + plt.ylabel("Theta (rad)") + plt.title("Theta vs Time") + plt.grid(True) + plt.show() + + + +def main(args=None): + rclpy.init(args=args) + node = ControllerNode() + + try: + + rclpy.spin(node) + + except KeyboardInterrupt: + + node.get_logger().info("Keyboard Interrupt detected...shutting the node down") + node.plot_graph() + node.destroy_node() + + + rclpy.shutdown() + + + + \ No newline at end of file diff --git a/double_inverted/config/config.rviz b/double_inverted/config/config.rviz new file mode 100644 index 0000000..f59fc28 --- /dev/null +++ b/double_inverted/config/config.rviz @@ -0,0 +1,130 @@ +Panels: + - Class: rviz_common/Displays + Help Height: 78 + Name: Displays + Property Tree Widget: + Expanded: + - /Global Options1 + - /Status1 + Splitter Ratio: 0.5 + Tree Height: 617 + - Class: rviz_common/Selection + Name: Selection + - Class: rviz_common/Tool Properties + Expanded: + - /2D Goal Pose1 + - /Publish Point1 + Name: Tool Properties + Splitter Ratio: 0.5886790156364441 + - Class: rviz_common/Views + Expanded: + - /Current View1 + Name: Views + Splitter Ratio: 0.5 +Visualization Manager: + Class: "" + Displays: + - Alpha: 0.5 + Cell Size: 1 + Class: rviz_default_plugins/Grid + Color: 160; 160; 164 + Enabled: true + Line Style: + Line Width: 0.029999999329447746 + Value: Lines + Name: Grid + Normal Cell Count: 0 + Offset: + X: 0 + Y: 0 + Z: 0 + Plane: XY + Plane Cell Count: 10 + Reference Frame: + Value: true + - Class: rviz_default_plugins/Marker + Enabled: true + Name: Marker + Namespaces: + "": true + Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /pendulum_viz + Value: true + Enabled: true + Global Options: + Background Color: 48; 48; 48 + Fixed Frame: map + Frame Rate: 30 + Name: root + Tools: + - Class: rviz_default_plugins/Interact + Hide Inactive Objects: true + - Class: rviz_default_plugins/MoveCamera + - Class: rviz_default_plugins/Select + - Class: rviz_default_plugins/FocusCamera + - Class: rviz_default_plugins/Measure + Line color: 128; 128; 0 + - Class: rviz_default_plugins/SetInitialPose + Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /initialpose + - Class: rviz_default_plugins/SetGoal + Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /goal_pose + - Class: rviz_default_plugins/PublishPoint + Single click: true + Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /clicked_point + Transformation: + Current: + Class: rviz_default_plugins/TF + Value: true + Views: + Current: + Angle: 0 + Class: rviz_default_plugins/TopDownOrtho + Enable Stereo Rendering: + Stereo Eye Separation: 0.05999999865889549 + Stereo Focal Distance: 1 + Swap Stereo Eyes: false + Value: false + Invert Z Axis: false + Name: Current View + Near Clip Distance: 0.009999999776482582 + Scale: 142.98838806152344 + Target Frame: + Value: TopDownOrtho (rviz_default_plugins) + X: 0 + Y: 0 + Saved: ~ +Window Geometry: + Displays: + collapsed: false + Height: 846 + Hide Left Dock: false + Hide Right Dock: false + QMainWindow State: 000000ff00000000fd000000040000000000000156000002f4fc0200000008fb0000001200530065006c0065006300740069006f006e00000001e10000009b0000005c00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073010000003d000002f4000000c900fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261000000010000010f000002f4fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073010000003d000002f4000000a400fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000004420000003efc0100000002fb0000000800540069006d00650100000000000004420000000000000000fb0000000800540069006d006501000000000000045000000000000000000000023f000002f400000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000 + Selection: + collapsed: false + Tool Properties: + collapsed: false + Views: + collapsed: false + Width: 1200 + X: 72 + Y: 60 diff --git a/double_inverted/double_inverted/__init__.py b/double_inverted/double_inverted/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/double_inverted/double_inverted/__pycache__/__init__.cpython-310.pyc b/double_inverted/double_inverted/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..2a2d37c Binary files /dev/null and b/double_inverted/double_inverted/__pycache__/__init__.cpython-310.pyc differ diff --git a/double_inverted/double_inverted/__pycache__/dynamics_double.cpython-310.pyc b/double_inverted/double_inverted/__pycache__/dynamics_double.cpython-310.pyc new file mode 100644 index 0000000..5078e8a Binary files /dev/null and b/double_inverted/double_inverted/__pycache__/dynamics_double.cpython-310.pyc differ diff --git a/double_inverted/double_inverted/dynamics_double.py b/double_inverted/double_inverted/dynamics_double.py new file mode 100644 index 0000000..b5450d2 --- /dev/null +++ b/double_inverted/double_inverted/dynamics_double.py @@ -0,0 +1,172 @@ +import numpy as np +import matplotlib.pyplot as plt +import time +from math import sin, cos + +import rclpy +from rclpy.node import Node + +from builtin_interfaces.msg import Duration +from visualization_msgs.msg import Marker +from geometry_msgs.msg import Point + +from custom_msgs.msg import TorqueInput, States + +class double_inverted_pendulum(Node): + + # Initialize State/s : at theta = pi/2, pi/2 + theta1_0 = np.pi / 2 + theta2_0 = np.pi / 2 + theta1_dot0 = 0.0 + theta2_dot0 = 0.0 + + # You can use similar variations as the original code for initialization + + + #theta is almost upright (randomized) + theta1_0 = np.pi - (np.random.rand() - 0.5) / 2 + theta1_0 = (theta1_0 + np.pi)%(2*np.pi) - np.pi + + theta2_0 = np.pi - (np.random.rand() - 0.5) / 2 + theta2_0 = (theta2_0 + np.pi)%(2*np.pi) - np.pi + + theta1_dot0 = 0 + theta2_dot0 = 0 + + + # Input/s + torque_value = 0.0 + + # Param/s + mass1 = 1.0 + mass2 = 1.0 + g = 9.81 + l1 = 1.0 + l2 = 1.0 + state_update_frequency = 500 + state_update_timeperiod = 1 / state_update_frequency + + feedback_frequency = 50 + + def __init__(self): + super().__init__('double_pendulum') + + # Timers + self.update_states_timer = self.create_timer(1 / self.state_update_frequency, self.update_pendulum_states) + #self.feedback_timer = self.create_timer(1 / self.feedback_frequency, self.feedback) + + # Publishers/Subscribers/Services + self.visualizer = self.create_publisher(Marker, '/pendulum_viz', 1) + self.feedback_pub = self.create_publisher(States, '/state_feedback', 1) + self.input = self.create_subscription(TorqueInput, '/torque_input', self.update_input_torque, 5) + + # Attributes + self.t_start = time.time() + self.t_prev = time.time() - 0.0001 + self.obj_id = 0 + + # States + self.theta1 = self.theta1_0 + self.theta2 = self.theta2_0 + self.theta1_dot = self.theta1_dot0 + self.theta2_dot = self.theta2_dot0 + + self.get_logger().info('Double Inverted Pendulum node initialized') + self.get_logger().info('Accepting Input') + self.get_logger().info('Publishing Feedback') + + def f(self, x, u): + m1, m2, l1, l2, g = self.mass1, self.mass2, self.l1, self.l2, self.g + theta1, theta2, theta1_dot, theta2_dot = x + + delta = theta2 - theta1 + denom1 = (m1 + m2) * l1 - m2 * l1 * cos(delta) ** 2 + denom2 = (l2 / l1) * denom1 + + theta1_ddot = (m2 * l1 * theta1_dot ** 2 * sin(delta) * cos(delta) + + m2 * g * sin(theta2) * cos(delta) + + m2 * l2 * theta2_dot ** 2 * sin(delta) - + (m1 + m2) * g * sin(theta1)) / denom1 + + theta2_ddot = (-m2 * l2 * theta2_dot ** 2 * sin(delta) * cos(delta) + + (m1 + m2) * g * sin(theta1) * cos(delta) - + (m1 + m2) * l1 * theta1_dot ** 2 * sin(delta) - + (m1 + m2) * g * sin(theta2)) / denom2 + + return np.array([theta1_dot, theta2_dot, theta1_ddot, theta2_ddot]) + + def update_pendulum_states(self): + dt = time.time() - self.t_prev + self.t_prev = time.time() + + x = np.array([self.theta1, self.theta2, self.theta1_dot, self.theta2_dot]) + x_intermediate = x + 0.5 * dt * self.f(x, self.torque_value) + x += dt * self.f(x_intermediate, self.torque_value) + + self.theta1, self.theta2, self.theta1_dot, self.theta2_dot = x + + self.visualize_pendulum() + return + + ''' + def feedback(self): + states_msg = States() + states_msg.theta1 = self.theta1 + states_msg.theta2 = self.theta2 + states_msg.theta1_dot = self.theta1_dot + states_msg.theta2_dot = self.theta2_dot + self.feedback_pub.publish(states_msg) + return + ''' + + def visualize_pendulum(self): + pendulum_marker = Marker() + pendulum_marker.header.frame_id = "map" + pendulum_marker.id = self.obj_id + pendulum_marker.type = Marker.LINE_STRIP + pendulum_marker.action = Marker.ADD + pendulum_marker.pose.orientation.w = 1.0 + pendulum_marker.scale.x = 0.05 # Line width + + point_1 = Point() + point_1.x = 0.0 + point_1.y = 0.0 + point_1.z = 0.0 + + point_2 = Point() + point_2.x = self.l1 * sin(self.theta1) + point_2.y = - self.l1 * cos(self.theta1) + point_2.z = 0.0 + + point_3 = Point() + point_3.x = point_2.x + self.l2 * sin(self.theta2) + point_3.y = point_2.y - self.l2 * cos(self.theta2) + point_3.z = 0.0 + + pendulum_marker.points = [point_1, point_2, point_3] + + pendulum_marker.color.r = 1.0 + pendulum_marker.color.a = 1.0 # Alpha value + Duration_of_pendulum_marker = Duration() + Duration_of_pendulum_marker.sec = 0 + Duration_of_pendulum_marker.nanosec = int(self.state_update_timeperiod * 1e+9) + pendulum_marker.lifetime = Duration_of_pendulum_marker + + self.visualizer.publish(pendulum_marker) + + self.obj_id += 1 + + def update_input_torque(self, msg): + self.torque_value = max(-5, min(5, msg.torque_value)) + return + +def main(args=None): + rclpy.init(args=args) + pendulum_ = double_inverted_pendulum() + rclpy.spin(pendulum_) + + pendulum_.destroy_node() + rclpy.shutdown() + +if __name__ == '__main__': + main() diff --git a/double_inverted/launch/double_inverted_pedulum.launch.py b/double_inverted/launch/double_inverted_pedulum.launch.py new file mode 100644 index 0000000..20bc0fb --- /dev/null +++ b/double_inverted/launch/double_inverted_pedulum.launch.py @@ -0,0 +1,28 @@ +from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument +from launch.substitutions import LaunchConfiguration +from launch_ros.actions import Node +import os + +def generate_launch_description(): + + package_dir = os.path.dirname(os.path.abspath(__file__)) + rviz_config_file = os.path.join(package_dir, '..', 'config', 'config.rviz') + + visualizer = Node( + package = 'rviz2', + executable = 'rviz2', + # name='rviz2', + arguments = ['-d', rviz_config_file] + + ) + + dynamics = Node( + package = 'double_inverted', + executable = 'dynamics_double' + ) + + return LaunchDescription([ + visualizer, + dynamics + ]) \ No newline at end of file diff --git a/double_inverted/package.xml b/double_inverted/package.xml new file mode 100644 index 0000000..bbf1c1d --- /dev/null +++ b/double_inverted/package.xml @@ -0,0 +1,20 @@ + + + + double_inverted + 0.0.0 + TODO: Package description + atharav + TODO: License declaration + + rclpy + + ament_copyright + ament_flake8 + ament_pep257 + python3-pytest + + + ament_python + + diff --git a/double_inverted/resource/double_inverted b/double_inverted/resource/double_inverted new file mode 100644 index 0000000..e69de29 diff --git a/double_inverted/setup.cfg b/double_inverted/setup.cfg new file mode 100644 index 0000000..2daf2b6 --- /dev/null +++ b/double_inverted/setup.cfg @@ -0,0 +1,4 @@ +[develop] +script_dir=$base/lib/double_inverted +[install] +install_scripts=$base/lib/double_inverted diff --git a/double_inverted/setup.py b/double_inverted/setup.py new file mode 100644 index 0000000..20d03f2 --- /dev/null +++ b/double_inverted/setup.py @@ -0,0 +1,32 @@ +from setuptools import find_packages, setup +import os +from glob import glob + +package_name = 'double_inverted' + +setup( + name=package_name, + version='0.0.0', + packages=[package_name], + data_files=[ + ('share/ament_index/resource_index/packages', + ['resource/' + package_name]), + ('share/' + package_name, ['package.xml']), + (os.path.join('share', package_name, 'launch/'), + glob('launch/*launch.[pxy][yma]*')), + (os.path.join('share', package_name, 'config/'), + glob('config/**')) + ], + install_requires=['setuptools'], + zip_safe=True, + maintainer='atharav', + maintainer_email='atharav.sonawane.aryo@gmail.com', + description='TODO: Package description', + license='TODO: License declaration', + tests_require=['pytest'], + entry_points={ + 'console_scripts': [ + 'dynamics_double = double_inverted.dynamics_double:main' + ], + }, +) diff --git a/double_inverted/test/test_copyright.py b/double_inverted/test/test_copyright.py new file mode 100644 index 0000000..97a3919 --- /dev/null +++ b/double_inverted/test/test_copyright.py @@ -0,0 +1,25 @@ +# Copyright 2015 Open Source Robotics Foundation, 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. + +from ament_copyright.main import main +import pytest + + +# Remove the `skip` decorator once the source file(s) have a copyright header +@pytest.mark.skip(reason='No copyright header has been placed in the generated source file.') +@pytest.mark.copyright +@pytest.mark.linter +def test_copyright(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found errors' diff --git a/double_inverted/test/test_flake8.py b/double_inverted/test/test_flake8.py new file mode 100644 index 0000000..27ee107 --- /dev/null +++ b/double_inverted/test/test_flake8.py @@ -0,0 +1,25 @@ +# Copyright 2017 Open Source Robotics Foundation, 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. + +from ament_flake8.main import main_with_errors +import pytest + + +@pytest.mark.flake8 +@pytest.mark.linter +def test_flake8(): + rc, errors = main_with_errors(argv=[]) + assert rc == 0, \ + 'Found %d code style errors / warnings:\n' % len(errors) + \ + '\n'.join(errors) diff --git a/double_inverted/test/test_pep257.py b/double_inverted/test/test_pep257.py new file mode 100644 index 0000000..b234a38 --- /dev/null +++ b/double_inverted/test/test_pep257.py @@ -0,0 +1,23 @@ +# Copyright 2015 Open Source Robotics Foundation, 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. + +from ament_pep257.main import main +import pytest + + +@pytest.mark.linter +@pytest.mark.pep257 +def test_pep257(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found code style errors / warnings'