-
Notifications
You must be signed in to change notification settings - Fork 41
Added Omnimove external control #152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ravirathnam
wants to merge
58
commits into
kroshu:master
Choose a base branch
from
ravirathnam:ungarn_merge
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
58 commits
Select commit
Hold shift + click to select a range
6abe941
working on external control driver
ravirathnam 34883c6
renamed class to external_control
ravirathnam cc335de
external control can be extended through configuration.
ravirathnam 052891c
added config for the controller
6436dfe
working on parsing messages
a51c83b
working on header
7fe1a11
omnimove messasges correctly being parsed
e408d4e
added caterpillar support
8ddf49b
changing startup to allow quantec support
6f66c51
agv type as parameter and setting interfaces using that
34c6e66
using agv_type to send drive commands
a31df54
merge with upstream
1cca280
Merge branch 'kroshu:master' into ungarn_merge
ravirathnam 84ba687
added caterpillar drive commands
3d4fba1
added test node for platform
2faa719
modified platform_move_test to move iontec as well
9251782
removed velocity controller for pillar and shield,
abbab90
changed test to send 0 velocities for caterpillar move.
271ee15
checking velocities for caterpillar so that it can also stop agv.
ede631f
undid check for caterpillar becasue velocity is 0 for lift movement
d68d7b5
added test package for robot movement
3d66521
added missing resource file
bd3ed0b
added all movements for tests
75a5817
added service for platform move test to allow endurance testing
3f751f9
rewriting node to do everything using callbacks instead of spinninng_…
9c15227
waiting for service to end using python threading. working for both s…
0d8697f
added qt and idea created files
d6e3082
Merge branch 'master' into ungarn_merge
ec66222
undo changes to rsi_driver.
ed5d9cb
removed unused file
4bdbfa2
separate script for iontec, changed quantec test to use new position
85e4fa6
modified test to always change velocity.
d07271c
increasing rotation speed for caterpillar
c139674
changes from pre-commit hook
77fe876
formatting using pre-commit
bd7c5f7
formatting changes from pre-commit
88bd47b
formatting splitting line
c90072b
update formatting
52334e2
changing formatting in platformMoveTest
583850d
formatting
17f9f00
Merge pull request #2 from kroshu/master
ravirathnam f733ef1
commenting out ament_cpp_lint in testing.
38be84b
Merge branch 'ungarn_merge' of https://github.com/ravirathnam/ros2_ku…
49a1d98
removing auto_lint_test from cmake
51513d2
Merge branch 'master' into ungarn_merge
ravirathnam 526df32
use static_cast
ravirathnam 8d7866a
changes from review.
55dd7ac
changes from review, added all pillars to one array
b3e0c8f
allowed pillar speeds to be negative
22cd585
returning error if socket can't connect
7b5f4cb
removed iontec and quantec tests and replaced with robot_movement_tes…
19d4667
removed unneeded configuration file.
142b66b
movements for test completely parametrisable, added configuration for…
637c0e1
renamed files
c760ea9
renamed directory to match package name
b4f558b
renamed driver to fit convention
0b3640e
formatting
d127f70
formatting
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,4 @@ | ||
| build* | ||
| CMake*.user | ||
| .idea | ||
| __pycache__/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| robot_manager_client: | ||
| ros__parameters: | ||
| home_pos: [0.0, -2.3911, 2.6529, 0.0, 1.308, 0.0] | ||
| times: [6, 12, 6, 10, 10, 10, 10, 5, 5, 5, 5, 10, 5, 5, 10, 5] | ||
| position_names: ["joint1_plus", "joint1_minus", "home", "joint2_plus", "home", "joint3_minus", "home", "joint4_plus", "joint4_minus", "home", "joint5_plus", "joint5_minus", "home", "joint6_plus", "joint6_minus", "home"] | ||
| joint_positions: | ||
| home: [0.0, -2.3911, 2.6529, 0.0, 1.308, 0.0] | ||
| joint1_plus: [0.4, -2.3911, 2.6529, 0.0, 1.308, 0.0] | ||
| joint1_minus: [-0.4, -2.3911, 2.6529, 0.0, 1.308, 0.0] | ||
| # joint2 only moving in one direction since we are at the limit | ||
| joint2_plus: [0.0, -1.9911, 2.6529, 0.0, 1.308, 0.0] | ||
| # joint3 only moving in one direction since we are at the limit | ||
| joint3_minus: [0.0, -2.3911, 2.2529, 0.0, 1.308, 0.0] | ||
| # joint4 | ||
| joint4_plus: [0.0, -2.3911, 2.6529, 0.4, 1.308, 0.0] | ||
| joint4_minus: [0.0, -2.3911, 2.6529, -0.4, 1.308, 0.0] | ||
| # joint5 | ||
| joint5_plus: [0.0, -2.3911, 2.6529, 0.0, 1.708, 0.0] | ||
| joint5_minus: [0.0, -2.3911, 2.6529, 0.0, 0.908, 0.0] | ||
| # joint6 | ||
| joint6_plus: [0.0, -2.3911, 2.6529, 0.0, 1.308, 0.4] | ||
| joint6_minus: [0.0, -2.3911, 2.6529, 0.0, 1.308, -0.4] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| robot_manager_client: | ||
| ros__parameters: | ||
| times: [6, 12, 6, 10, 10, 10, 10, 5, 5, 5, 5, 10, 5, 5, 10, 5] | ||
| position_names: ["joint1_plus", "joint1_minus", "home", "joint2_plus", "home", "joint3_minus", "home", "joint4_plus", "joint4_minus", "home", "joint5_plus", "joint5_minus", "home", "joint6_plus", "joint6_minus", "home"] | ||
| joint_positions: | ||
| home: [0.0299, -2.38422, 2.3471, 0.0, 1.5943, -2.83578] | ||
| joint1_plus: [0.4299, -2.38422, 2.3471, 0.0, 1.5943, -2.83578] | ||
| joint1_minus: [-0.4299, -2.38422, 2.3471, 0.0, 1.5943, -2.83578] | ||
| # joint2 only moving in one direction since we are at the limit | ||
| joint2_plus: [0.0299, -1.98422, 2.3471, 0.0, 1.5943, -2.83578] | ||
| # joint3 only moving in one direction since we are at the limit | ||
| joint3_minus: [0.0299, -2.38422, 1.9471, 0.0, 1.5943, -2.83578] | ||
| # joint4 | ||
| joint4_plus: [0.0299, -2.38422, 2.3471, 0.4, 1.5943, -2.83578] | ||
| joint4_minus: [0.0299, -2.38422, 2.3471, -0.4, 1.5943, -2.83578] | ||
| # joint5 | ||
| joint5_plus: [0.0299, -2.38422, 2.3471, 0.0, 1.9943, -2.83578] | ||
| joint5_minus: [0.0299, -2.38422, 2.3471, 0.0, 1.1943, -2.83578] | ||
| # joint6 | ||
| joint6_plus: [0.0299, -2.38422, 2.3471, 0.0, 1.5943, -2.43578] | ||
| joint6_minus: [0.0299, -2.38422, 2.3471, 0.0, 1.5943, -3.23578] |
Empty file.
208 changes: 208 additions & 0 deletions
208
examples/kuka_rsi_robot_tests/kuka_rsi_robot_tests/robot_movement_test.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,208 @@ | ||
| import copy | ||
|
|
||
| import rclpy | ||
| import std_srvs.srv | ||
| from builtin_interfaces.msg import Duration | ||
| from rclpy.node import Node | ||
| from lifecycle_msgs.srv import ChangeState, GetState | ||
| from lifecycle_msgs.msg import Transition, State | ||
| from control_msgs.action import FollowJointTrajectory | ||
| from trajectory_msgs.msg import JointTrajectory, JointTrajectoryPoint | ||
| from rclpy.action import ActionClient | ||
| from rclpy.callback_groups import MutuallyExclusiveCallbackGroup | ||
| from threading import Event | ||
|
|
||
|
|
||
| class RobotManagerClient(Node): | ||
| def __init__(self, service_group): | ||
| super().__init__("robot_manager_client") | ||
| self.moves = None | ||
| self.current_index = None | ||
| self.actionFinished = None | ||
| self.declare_parameter("start_service", False) | ||
| self.start_service = self.get_parameter("start_service").get_parameter_value().bool_value | ||
| self.action_client = None | ||
| self.start_movement_service = None | ||
| self.movement_done = Event() | ||
| self.declare_parameter("position_names", [""]) | ||
| position_names = ( | ||
| self.get_parameter("position_names").get_parameter_value().string_array_value | ||
| ) | ||
| self.declare_parameter("times", [int()]) | ||
| self.times = self.get_parameter("times").get_parameter_value().integer_array_value | ||
| self.get_logger().info(f"position names is {position_names}") | ||
| self.get_logger().info(f"times is {self.times}") | ||
| if self.start_service is True: | ||
| self.service_group = service_group | ||
| self.start_movement_service = self.create_service( | ||
| std_srvs.srv.Empty, | ||
| "start_robot_movements", | ||
| self.start_robot_movements_callback, | ||
| callback_group=service_group, | ||
| ) | ||
| self.stop_movement_service = self.create_service( | ||
| std_srvs.srv.Empty, | ||
| "stop_robot_movements", | ||
| self.stop_robot_movements_callback, | ||
| callback_group=service_group, | ||
| ) | ||
| state_client_group = MutuallyExclusiveCallbackGroup() | ||
| self.cli = self.create_client( | ||
| ChangeState, "/robot_manager/change_state", callback_group=state_client_group | ||
| ) | ||
| self.state_client = self.create_client( | ||
| GetState, "/robot_manager/get_state", callback_group=state_client_group | ||
| ) | ||
| while not self.cli.wait_for_service(timeout_sec=1.0): | ||
| self.get_logger().info("service not available, waiting again...") | ||
| self.req = ChangeState.Request() | ||
| action_client_group = MutuallyExclusiveCallbackGroup() | ||
| self.action_client = ActionClient( | ||
| self, | ||
| FollowJointTrajectory, | ||
| "/joint_trajectory_controller/follow_joint_trajectory", | ||
| callback_group=action_client_group, | ||
| ) | ||
| self.create_movements(position_names) | ||
|
|
||
| def create_movements(self, position_names): | ||
| self.moves = [] | ||
| for position_name in position_names: | ||
| param_name = "joint_positions." + position_name | ||
| if not self.has_parameter(param_name): | ||
| self.declare_parameter(param_name, [float()]) | ||
| self.moves.append( | ||
| self.get_parameter(param_name).get_parameter_value().double_array_value | ||
| ) | ||
|
|
||
| self.get_logger().info(f"moves are {self.moves}") | ||
|
|
||
| def send_configure(self): | ||
| self.req.transition.id = Transition.TRANSITION_CONFIGURE | ||
| while not self.cli.wait_for_service(timeout_sec=1.0): | ||
| self.get_logger().info("service not available, waiting again...") | ||
| self.get_logger().info("calling configure") | ||
| future = self.cli.call_async(self.req) | ||
| future.add_done_callback(self.configure_done_callback) | ||
|
|
||
| def configure_done_callback(self, future): | ||
| self.get_logger().info("configure done. Calling activate") | ||
| self.send_activate() | ||
|
|
||
| def send_activate(self): | ||
| self.req.transition.id = Transition.TRANSITION_ACTIVATE | ||
| future = self.cli.call_async(self.req) | ||
| future.add_done_callback(self.activate_done_callback) | ||
|
|
||
| def send_deactivate(self): | ||
| self.req.transition.id = Transition.TRANSITION_DEACTIVATE | ||
| future = self.cli.call_async(self.req) | ||
| future.add_done_callback(self.deactivate_done_callback) | ||
|
|
||
| def activate_done_callback(self, future): | ||
| self.get_logger().info("activate done") | ||
| self.get_state() | ||
|
|
||
| def deactivate_done_callback(self, future): | ||
| self.get_logger().info( | ||
| "deactivate call done. If it failed, probably manual intervention is required" | ||
| ) | ||
| self.movement_done.set() | ||
|
|
||
| def activate_robot(self): | ||
| self.send_configure() # this will in callback send the activate | ||
|
|
||
| def get_state(self): | ||
| state_future = self.state_client.call_async(GetState.Request()) | ||
| state_future.add_done_callback(self.get_state_callback) | ||
| self.get_logger().info("waiting for getting state") | ||
|
|
||
| def get_state_callback(self, future): | ||
| self.get_logger().info("got current state") | ||
| state_resp = future.result() | ||
| if state_resp.current_state.id == State.PRIMARY_STATE_ACTIVE: | ||
| self.get_logger().info("Robot is active") | ||
| self.current_index = 0 | ||
| self.start_next_movement() | ||
| else: | ||
| self.get_logger().error("could not activate robot") | ||
| if self.start_service: | ||
| self.movement_done.set() | ||
| else: | ||
| self.executor.shutdown() | ||
|
|
||
| def start_next_movement(self): | ||
| if self.current_index < len(self.times): | ||
| self.get_logger().info("going to submit movement") | ||
| position = self.moves[self.current_index] | ||
| duration = self.times[self.current_index] | ||
| self.current_index = self.current_index + 1 | ||
| self.start_robot_movement(position, duration) | ||
| else: | ||
| self.get_logger().info("All movements done") | ||
| if self.start_service: | ||
| self.movement_done.set() | ||
| else: | ||
| self.executor.shutdown() | ||
|
|
||
| def start_robot_movement(self, position, duration): | ||
| goal = FollowJointTrajectory.Goal() | ||
| trajectory = JointTrajectory() | ||
| trajectory.joint_names = ["joint_1", "joint_2", "joint_3", "joint_4", "joint_5", "joint_6"] | ||
| goal_point = JointTrajectoryPoint() | ||
| goal_point.time_from_start = Duration(sec=duration) | ||
| goal_point.positions = position | ||
| trajectory.points.append(copy.deepcopy(goal_point)) | ||
| goal.trajectory = trajectory | ||
| self.action_client.wait_for_server() | ||
| cur_action_submit = self.action_client.send_goal_async(goal) | ||
| self.get_logger().info("waiting for action to be submitted") | ||
| # self.executor.spin_until_future_complete(cur_action_submit) | ||
| cur_action_submit.add_done_callback(self.action_submit_callback) | ||
|
|
||
| def action_submit_callback(self, future): | ||
| self.get_logger().info("action submitted, waiting for action to be done") | ||
| result_action = future.result().get_result_async() | ||
| result_action.add_done_callback(self.action_done_callback) | ||
|
|
||
| def action_done_callback(self, future): | ||
| self.get_logger().info("action finished in callback") | ||
| self.start_next_movement() | ||
|
|
||
| def do_robot_movements(self): | ||
| self.activate_robot() | ||
|
|
||
| def start_robot_movements_callback(self, request, response): | ||
| self.get_logger().info("Processing start movement service call") | ||
| self.movement_done.clear() | ||
| self.do_robot_movements() | ||
| self.get_logger().info("waiting for all actions to be done") | ||
| self.movement_done.wait() | ||
| self.get_logger().info("service returning") | ||
| return response | ||
|
|
||
| def stop_robot_movements_callback(self, request, response): | ||
| self.get_logger().info("Processing start movement service call") | ||
| self.movement_done.clear() | ||
| self.send_deactivate() | ||
| self.movement_done.wait() | ||
| return response | ||
|
|
||
|
|
||
| def main(args=None): | ||
| rclpy.init(args=args) | ||
| service_group = rclpy.callback_groups.MutuallyExclusiveCallbackGroup() | ||
| robot_manager_client = RobotManagerClient(service_group) | ||
| executor = rclpy.executors.MultiThreadedExecutor() | ||
| executor.add_node(robot_manager_client) | ||
| if robot_manager_client.start_service is False: | ||
| robot_manager_client.do_robot_movements() | ||
| executor.spin() | ||
| robot_manager_client.destroy_node() | ||
| else: | ||
| executor.spin() | ||
| rclpy.shutdown() | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| <?xml version="1.0"?> | ||
| <?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?> | ||
| <package format="3"> | ||
| <name>kuka_rsi_robot_tests</name> | ||
| <version>0.0.1</version> | ||
| <description>This package contains test nodes which connect to a kuka rsi robot and runs some basic movements</description> | ||
| <maintainer email="ravi.rathnam@kuka.com">ubuntu</maintainer> | ||
| <license>Apache License 2.0</license> | ||
| <depend>rclpy</depend> | ||
| <depend>lifecycle_msgs</depend> | ||
| <depend>joint_trajectory_controller</depend> | ||
| <test_depend>ament_copyright</test_depend> | ||
| <test_depend>ament_flake8</test_depend> | ||
| <test_depend>ament_pep257</test_depend> | ||
| <test_depend>python3-pytest</test_depend> | ||
|
|
||
| <export> | ||
| <build_type>ament_python</build_type> | ||
| </export> | ||
| </package> | ||
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| [develop] | ||
| script_dir=$base/lib/kuka_rsi_robot_tests | ||
| [install] | ||
| install_scripts=$base/lib/kuka_rsi_robot_tests |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| from setuptools import find_packages, setup | ||
|
|
||
| package_name = "kuka_rsi_robot_tests" | ||
|
|
||
| setup( | ||
| name=package_name, | ||
| version="0.0.0", | ||
| packages=find_packages(exclude=["test"]), | ||
| data_files=[ | ||
| ("share/ament_index/resource_index/packages", ["resource/" + package_name]), | ||
| ("share/" + package_name, ["package.xml"]), | ||
| ], | ||
| install_requires=["setuptools"], | ||
| zip_safe=True, | ||
| maintainer="KUKA Deutschland GmbH", | ||
| maintainer_email="ravi.rathnam@kuka.com", | ||
| description="This package contains test nodes which connect to a " | ||
| "kuka rsi robot and runs some basic movements", | ||
| license="Apache License 2.0", | ||
| tests_require=["pytest"], | ||
| entry_points={ | ||
| "console_scripts": [ | ||
| "robot_movement_test = kuka_rsi_robot_tests.robot_movement_test:main", | ||
| ], | ||
| }, | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| cmake_minimum_required(VERSION 3.8) | ||
| project(kuka_utv3_ec_driver) | ||
|
|
||
| if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") | ||
| add_compile_options(-Wall -Wextra -Wpedantic) | ||
| endif() | ||
|
|
||
| # find dependencies | ||
| find_package(ament_cmake REQUIRED) | ||
| # uncomment the following section in order to fill in | ||
| # further dependencies manually. | ||
| find_package(hardware_interface REQUIRED) | ||
| find_package(pluginlib REQUIRED) | ||
| find_package(rclcpp REQUIRED) | ||
| find_package(rclcpp_lifecycle REQUIRED) | ||
| find_package(std_srvs REQUIRED) | ||
| find_package(Boost REQUIRED COMPONENTS chrono) | ||
|
|
||
| set(THIS_PACKAGE_INCLUDE_DEPENDS | ||
| hardware_interface | ||
| pluginlib | ||
| rclcpp | ||
| rclcpp_lifecycle | ||
| ) | ||
|
|
||
| include_directories(include) | ||
|
|
||
|
|
||
| ## COMPILE | ||
| add_library( | ||
| kuka_utv3_ec_driver | ||
| SHARED | ||
| src/kuka_utv3_hardware_interface.cpp | ||
| src/external_control_message.cpp | ||
| ) | ||
|
|
||
| add_executable( | ||
| platform_move_test | ||
| src/platform_move_test.cpp | ||
| ) | ||
| ament_target_dependencies( | ||
| platform_move_test | ||
| rclcpp std_msgs std_srvs | ||
| ) | ||
|
|
||
| install(TARGETS kuka_utv3_ec_driver | ||
| DESTINATION lib) | ||
|
|
||
| install(TARGETS platform_move_test | ||
| DESTINATION lib/${PROJECT_NAME}) | ||
|
|
||
|
|
||
| ament_target_dependencies(kuka_utv3_ec_driver PUBLIC ${THIS_PACKAGE_INCLUDE_DEPENDS} Boost) | ||
| pluginlib_export_plugin_description_file(hardware_interface kuka_utv3_ec_driver.xml) | ||
|
|
||
| if(BUILD_TESTING) | ||
| #find_package(ament_lint_auto REQUIRED) | ||
| # the following line skips the linter which checks for copyrights | ||
| # comment the line when a copyright and license is added to all source files | ||
| set(ament_cmake_copyright_FOUND TRUE) | ||
| # the following line skips cpplint (only works in a git repo) | ||
| # comment the line when this package is in a git repo and when | ||
| # a copyright and license is added to all source files | ||
| set(ament_cmake_cpplint_FOUND TRUE) | ||
| #ament_lint_auto_find_test_dependencies() | ||
| endif() | ||
| ament_export_libraries( | ||
| kuka_utv3_ec_driver | ||
| ${BOOST_LIBRARIES} | ||
| ) | ||
| ament_package() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.