diff --git a/elevator_operation/CMakeLists.txt b/elevator_operation/CMakeLists.txt new file mode 100644 index 0000000000..7ee14907dc --- /dev/null +++ b/elevator_operation/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.0.2) +project(elevator_operation) + +find_package(catkin REQUIRED genmsg actionlib_msgs) + +add_message_files(DIRECTORY msg FILES DoorState.msg) + +add_service_files(DIRECTORY srv FILES LookAtTarget.srv) + +add_action_files(DIRECTORY action FILES MoveFloorWithElevator.action) + +generate_messages(DEPENDENCIES actionlib_msgs) + +catkin_package() diff --git a/elevator_operation/README.md b/elevator_operation/README.md new file mode 100644 index 0000000000..989d8988bf --- /dev/null +++ b/elevator_operation/README.md @@ -0,0 +1,43 @@ +# elevator_operation + +This package provides elevator operation and recognition interface to robot. + +## Setup + +Basically, dependencies are installed with rosdep. + +If you want to use spinal as altitude input, please install `spinal` package from jsk_aerial_robot. https://github.com/jsk-ros-pkg/jsk_aerial_robot/tree/master/aerial_robot_nerve/spinal + +## How to run + +Connect spinal or M5Stack_ROS ENVIII to PC and start `elevator_state_publisher.launch` + +```bash +roslaunch elevator_operation elevator_state_publisher.launch device_type:= device_name:= robot_type:= +``` + +And run elevator_operation node + +```bash +roslaunch elevator_operation elevator_operation.launch input_topic_points:= robot_type:= launch_switchbot_client:= switchbot_token_yaml:= +``` + +Then call action. + +```bash +rostopic pub -1 TODO +``` + +## How to run on Fetch1075 + +```bash +roslaunch elevator_operation elevator_state_publisher.launch device_type:=fetch1075_enviii robot_type:=fetch +``` + +```bash +roslaunch elevator_operation elevator_operation.launch input_topic_points:=/head_camera/depth_registered/points robot_type:=fetch +``` + +## How to configure + +To configure door frames, please edit [`doors_frame_publisher.launch`](./launch/doors_frame_publisher.launch). diff --git a/elevator_operation/action/MoveFloorWithElevator.action b/elevator_operation/action/MoveFloorWithElevator.action new file mode 100644 index 0000000000..7bb4e8797a --- /dev/null +++ b/elevator_operation/action/MoveFloorWithElevator.action @@ -0,0 +1,4 @@ +string target_floor_name +--- +bool success +--- diff --git a/elevator_operation/config/config.yaml b/elevator_operation/config/config.yaml new file mode 100644 index 0000000000..8a8bd53c71 --- /dev/null +++ b/elevator_operation/config/config.yaml @@ -0,0 +1,66 @@ +- floor: 8 + altitude: 29.6 + map_name: '/eng2/8f' +- floor: 7 + altitude: 25.8 + map_name: '/eng2/7f' + floor_name: 'eng2/7f' + door_frame_id: '/eng2/7f/elevator_door' + door_dimensions: [1.0, 0.5, 1.0] + outside_pose: + frame_id: 'map' + position: [2.631, -30.344, -0.000] + orientation: [0.000, 0.000, -0.861, 0.509] + inside_pose: + frame_id: 'map' + position: [1.634, -33.990, -0.000] + orientation: [-0.000, -0.000, 0.476, 0.879] + buttons: + down: '/eng2/7f/elevator_bot_down' +- floor: 6 + altitude: 22.0 + map_name: '/eng2/6f' +- floor: 5 + altitude: 16.8 + map_name: '/eng2/5f' +- floor: 4 + altitude: 13.1 + map_name: '/eng2/4f' +- floor: 3 + altitude: 9.1 + map_name: '/eng2/3f' + floor_name: 'eng2/3f' + door_frame_id: '/eng2/3f/elevator_door' + door_dimensions: [1.0, 0.5, 1.0] + outside_pose: + frame_id: 'map' + position: [2.631, -30.344, -0.000] + orientation: [0.000, 0.000, -0.861, 0.509] + inside_pose: + frame_id: 'map' + position: [1.634, -33.990, -0.000] + orientation: [-0.000, -0.000, 0.476, 0.879] + buttons: + up: '/eng2/3f/elevator_bot_up' +- floor: 2 + altitude: 5.0 + map_name: '/eng2/2f' +- floor: 1 + altitude: 0 + map_name: '/eng2/1f' + floor_name: 'eng2/1f' + door_frame_id: '/eng2/1f/elevator_door' + door_dimensions: [1.0, 0.5, 1.0] + outside_pose: + frame_id: 'map' + position: [2.631, -30.344, -0.000] + orientation: [0.000, 0.000, -0.861, 0.509] + inside_pose: + frame_id: 'map' + position: [1.634, -33.990, -0.000] + orientation: [-0.000, -0.000, 0.476, 0.879] + buttons: + up: '/eng2/1f/elevator_bot_up' +- floor: -1 + altitude: -4 + map_name: '/eng2/1f' diff --git a/elevator_operation/euslisp/look-at-elevator.l b/elevator_operation/euslisp/look-at-elevator.l new file mode 100755 index 0000000000..1bb0d91076 --- /dev/null +++ b/elevator_operation/euslisp/look-at-elevator.l @@ -0,0 +1,36 @@ +#!/usr/bin/env roseus + +(ros::roseus-add-srvs "elevator_operation") +(ros::roseus-add-msgs "move_base_msgs") + +(load "package://elevator_operation/euslisp/utils.l") + + +(defun handler-start-look-at-target (req) + (let (success + (res (instance elevator_operation::LookAtTargetResponse :init))) + (setq success (look-at-target-frame (send req :frame_id) :robot-type *robot-type*)) + (send res :success success) + res)) + +(defun main() + (ros::roseus "look_at_elevator_server") + (setq *robot-type* (ros::get-param "~robot_type" "")) + (cond + ((equal *robot-type* "fetch") + (load "package://fetcheus/fetch-interface.l") + (load "package://jsk_fetch_startup/euslisp/navigation-utils.l") + (fetch-init) + (setq *robot* *fetch*)) + (t + (setq *move-base-action-client* (instance ros::simple-action-client :init "/move_base" move_base_msgs::MoveBaseAction)) + (send *move-base-action-client* :wait-for-server) + ) + ) + (setq *tfl* (instance ros::transform-listener :init)) + (ros::advertise-service "~look_at_target" elevator_operation::LookAtTarget #'handler-start-look-at-target) + (ros::ros-info "initialized") + (ros::spin) + ) + +(main) diff --git a/elevator_operation/euslisp/utils.l b/elevator_operation/euslisp/utils.l new file mode 100644 index 0000000000..6f13199655 --- /dev/null +++ b/elevator_operation/euslisp/utils.l @@ -0,0 +1,98 @@ +(ros::roseus-add-msgs "move_base_msgs") +(ros::load-ros-manifest "move_base_msgs") +(ros::roseus-add-srvs "elevator_operation") +(ros::load-ros-manifest "elevator_operation") + +(defun update-model (&key (robot-type "fetch")) + (if (equal robot-type "fetch") + (send *robot* :angle-vector (send *ri* :state :potentio-vector))) + ) + +(defun look-at-target-with-head (coords-base-to-target &key (robot-type "fetch")) + (if (equal robot-type "fetch") + (progn + (send *robot* :look-at-target coords-base-to-target) + (send *ri* :angle-vector-raw (send *robot* :angle-vector) 500))) + ) + +(defun send-move-base-goal (x y z qx qy qz qw frame-id &key (wait t) (timeout 60)) + (if (not (boundp `*move-base-action-client*)) + (setq *move-base-action-client* (instance ros::simple-action-client :init "/move_base" move_base_msgs::MoveBaseAction)) + (ros::ros-warn "create move-base-action-client") + (ros::spin-once) + (send *move-base-action-client* :wait-for-server) + ) + (let ((time-stamp (ros::time-now)) + (goal (instance move_base_msgs::MoveBaseActionGoal :init))) + (ros::ros-warn "got a goal x: ~A, y: ~A, z: ~A, qx: ~A, qy: ~A, qz: ~A, qw: ~A on frame ~A" + x y z qx qy qz qw frame-id) + (send goal :header :stamp time-stamp) + (send goal :goal :target_pose :header :frame_id frame-id) + (send goal :goal :target_pose :pose :position :x x) + (send goal :goal :target_pose :pose :position :y y) + (send goal :goal :target_pose :pose :position :z z) + (send goal :goal :target_pose :pose :orientation :x qx) + (send goal :goal :target_pose :pose :orientation :y qy) + (send goal :goal :target_pose :pose :orientation :z qz) + (send goal :goal :target_pose :pose :orientation :w qw) + (send *move-base-action-client* :send-goal goal) + (ros::spin-once) + (ros::ros-warn "send a goal") + (if wait + (progn + (ros::ros-warn "waiting for a result of a goal") + (send *move-base-action-client* :wait-for-result :timeout timeout) + ))) + ) + +(defun rotate-base (theta &key (robot-type "fetch")) + (ros::ros-warn "rotating base ~A radians" theta) + (cond + ((equal robot-type "fetch") + (send *ri* :go-pos 0 0 (rad2deg theta))) + (t + (send-move-base-goal 0 0 0 0 0 (sin (/ theta 2)) (cos (/ theta 2)) "base_link" :wait t)) + )) + +(defun look-at-target-frame (target-frame-id &key (robot-type "fetch")) + (ros::ros-warn "target-frame-id: ~A" target-frame-id) + (let ((coords-base-to-target (send *tfl* :lookup-transform "base_link" target-frame-id (ros::time)))) + (update-model :robot-type robot-type) + (ros::ros-info "coords-base-to-target: ~A" coords-base-to-target) + (if (not coords-base-to-target) + (return-from look-at-target nil) + ) + (let* ((target-x (elt (send coords-base-to-target :pos) 0)) + (target-y (elt (send coords-base-to-target :pos) 1))) + (rotate-base (atan target-y target-x) :robot-type robot-type) + (update-model :robot-type robot-type) + (setq coords-base-to-target (send *tfl* :lookup-transform "base_link" target-frame-id (ros::time))) + (if (not coords-base-to-target) + (return-from look-at-target nil) + ) + (look-at-target-with-head coords-base-to-target :robot-type robot-type) + ) + t)) + +(defun move-elevator (target-floor-name) + (if (not (boundp `*move-elevator-action-client*)) + (progn + (setq *move-elevator-action-client* + (instance ros::simple-action-client :init + "/elevator_operation_server/move_floor_with_elevator" + elevator_operation::MoveFloorWithElevatorAction)) + (send *move-elevator-action-client* :wait-for-server))) + (let (result (goal (instance elevator_operation::MoveFloorWithElevatorGoal :init))) + (send goal :target_floor_name target-floor-name) + (send *move-elevator-action-client* :send-goal goal) + (send *move-elevator-action-client* :wait-for-result) + (setq result (send *move-elevator-action-client* :get-result)) + (send result :success)) + ) + +(defun call-look-at-target (topic-name frame-id) + (let (res (req (instance elevator_operation::LookAtTargetRequest :init))) + (send req :frame_id frame-id) + (setq res (ros::service-call topic-name req t)) + (send res :success) + )) diff --git a/elevator_operation/launch/doors_frame_publisher.launch b/elevator_operation/launch/doors_frame_publisher.launch new file mode 100644 index 0000000000..2a50edf64f --- /dev/null +++ b/elevator_operation/launch/doors_frame_publisher.launch @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/elevator_operation/launch/drivers/enviii_driver.launch b/elevator_operation/launch/drivers/enviii_driver.launch new file mode 100644 index 0000000000..d197da107f --- /dev/null +++ b/elevator_operation/launch/drivers/enviii_driver.launch @@ -0,0 +1,19 @@ + + + + + + + + baud: 115200 + port: $(arg device) + + + + + + + + + + diff --git a/elevator_operation/launch/drivers/fetch1075_enviii_driver.launch b/elevator_operation/launch/drivers/fetch1075_enviii_driver.launch new file mode 100644 index 0000000000..e7341173cb --- /dev/null +++ b/elevator_operation/launch/drivers/fetch1075_enviii_driver.launch @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/elevator_operation/launch/drivers/spinal_driver.launch b/elevator_operation/launch/drivers/spinal_driver.launch new file mode 100644 index 0000000000..4edb0081fa --- /dev/null +++ b/elevator_operation/launch/drivers/spinal_driver.launch @@ -0,0 +1,22 @@ + + + + + + + + + baud: 921600 + port: $(arg device_spinal) + + + + + + + diff --git a/elevator_operation/launch/elevator_door_detector.launch b/elevator_operation/launch/elevator_door_detector.launch new file mode 100644 index 0000000000..77e26bde61 --- /dev/null +++ b/elevator_operation/launch/elevator_door_detector.launch @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + initial_pos: $(arg door_position_offset) + initial_rot: $(arg door_rotation_offset) + dimension_x: $(arg door_dimension_x) + dimension_y: $(arg door_dimension_y) + dimension_z: $(arg door_dimension_z) + frame_id: $(arg elevator_door_frame_id) + + + + + + + + + + + + + diff --git a/elevator_operation/launch/elevator_operation.launch b/elevator_operation/launch/elevator_operation.launch new file mode 100644 index 0000000000..6603a376fb --- /dev/null +++ b/elevator_operation/launch/elevator_operation.launch @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + input_topic_points: $(arg input_topic_points) + global_costmap_inflation_plugin: '/move_base/global_costmap/inflater' + local_costmap_inflation_plugin: '/move_base/local_costmap/inflater' + #global_costmap_inflation_plugin: '/move_base/global_costmap/inflation_layer' + #local_costmap_inflation_plugin: '/move_base/local_costmap/inflation_layer' + + + + + + + + robot_type: $(arg robot_type) + + + + + + + + + + + diff --git a/elevator_operation/launch/elevator_state_publisher.launch b/elevator_operation/launch/elevator_state_publisher.launch new file mode 100644 index 0000000000..6935bd6213 --- /dev/null +++ b/elevator_operation/launch/elevator_state_publisher.launch @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + initial_floor: $(arg initial_floor) + threshold_altitude: 1 + threshold_accel: 0.2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + freq_cutoff: 0.5 + + + + diff --git a/elevator_operation/msg/DoorState.msg b/elevator_operation/msg/DoorState.msg new file mode 100644 index 0000000000..53b5d57858 --- /dev/null +++ b/elevator_operation/msg/DoorState.msg @@ -0,0 +1,5 @@ +uint8 UNKNOWN=0 +uint8 OPEN=1 +uint8 CLOSE=2 + +uint8 state diff --git a/elevator_operation/node_scripts/altitude_calculator.py b/elevator_operation/node_scripts/altitude_calculator.py new file mode 100755 index 0000000000..0e4f0815a7 --- /dev/null +++ b/elevator_operation/node_scripts/altitude_calculator.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python + +import rospy +import math +from std_msgs.msg import Float32 +from std_srvs.srv import Trigger, TriggerResponse +import message_filters + + +class AltitudeCalculator(object): + + def __init__(self): + + self.pub = rospy.Publisher('~output', Float32, queue_size=1) + + sub_pressure = message_filters.Subscriber('~input_pressure', Float32) + sub_temparature = message_filters.Subscriber( + '~input_temperature', Float32) + + self.altitude = 0 + self.altiude_offset = 0 + self.srv = rospy.Service('~zero_offset', Trigger, self.handler) + + # Pa + self.p_b = rospy.get_param('~sea_level_pressure', 101325) + # J K^-1 mol^-1 + self.gas_constant = rospy.get_param('~gas_const', 8.31446261815324) + # m sec^-2 + self.grav_accel = rospy.get_param('~gravitational_accel', 9.8) + + slop = rospy.get_param('~slop', 0.1) + self.ts = message_filters.ApproximateTimeSynchronizer( + [sub_pressure, sub_temparature], + 10, + slop=slop, + allow_headerless=True + ) + self.ts.registerCallback(self.callback) + + rospy.loginfo('initialized') + + def handler(self, req): + + self.altiude_offset = self.altitude + return TriggerResponse(success=True) + + def callback(self, msg_pressure, msg_temp): + + p = msg_pressure.data + T = msg_temp.data + altitude = (math.pow(self.p_b / p, 1 / 5.257) - 1) * \ + (T + 273.15) / 0.0065 + self.altitude = altitude + msg = Float32() + msg.data = self.altitude - self.altiude_offset + self.pub.publish(msg) + + +if __name__ == '__main__': + + rospy.init_node('altitude_calculator') + node = AltitudeCalculator() + rospy.spin() diff --git a/elevator_operation/node_scripts/elevator_door_opening_checker.py b/elevator_operation/node_scripts/elevator_door_opening_checker.py new file mode 100755 index 0000000000..cb53cbc294 --- /dev/null +++ b/elevator_operation/node_scripts/elevator_door_opening_checker.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python + +import rospy +from sensor_msgs.msg import PointCloud2 +from std_msgs.msg import Int64 +from elevator_operation.msg import DoorState + + +class ElevatorDoorOpeningCheckerNode(object): + + def __init__(self): + + self.threshold = rospy.get_param('~threshold', 100) + self.pub = rospy.Publisher('~output', Int64, queue_size=1) + self.pub_door_open = rospy.Publisher('~door_state', DoorState, queue_size=1) + self.sub = rospy.Subscriber('~input', PointCloud2, self.callback) + + def callback(self, msg): + + rospy.logdebug('door points: {}'.format(len(msg.data))) + self.pub.publish(Int64(data=len(msg.data))) + if len(msg.data) < self.threshold: + self.pub_door_open.publish(DoorState(state=DoorState.OPEN)) + else: + self.pub_door_open.publish(DoorState(state=DoorState.CLOSE)) + + +if __name__ == '__main__': + + rospy.init_node('elevator_door_opening_checker_node') + node = ElevatorDoorOpeningCheckerNode() + rospy.spin() diff --git a/elevator_operation/node_scripts/elevator_operation_server.py b/elevator_operation/node_scripts/elevator_operation_server.py new file mode 100755 index 0000000000..7415fb6799 --- /dev/null +++ b/elevator_operation/node_scripts/elevator_operation_server.py @@ -0,0 +1,369 @@ +#!/usr/bin/env python + +import actionlib +import rospy +import roslaunch +import rospkg +from switchbot_ros.switchbot_ros_client import SwitchBotROSClient + +import dynamic_reconfigure.client + +from std_srvs.srv import Trigger +from std_msgs.msg import Int16 +from std_msgs.msg import String +from elevator_operation.msg import DoorState + +from elevator_operation.srv import LookAtTarget + +from move_base_msgs.msg import MoveBaseAction +from move_base_msgs.msg import MoveBaseGoal +from elevator_operation.msg import MoveFloorWithElevatorAction +from elevator_operation.msg import MoveFloorWithElevatorResult + + +class ElevatorOperationServer(object): + + def __init__(self): + + ####################################################################### + # ROSLaunch + ####################################################################### + roslaunch.pmon._init_signal_handlers() + self.roslaunch_parent = None + + ####################################################################### + # Elevator Config + ####################################################################### + elevator_config = rospy.get_param('~elevator_config', []) + self.elevator_config = {entry['floor']: entry for entry in elevator_config} + rospy.logwarn('elevator config: {}'.format(self.elevator_config)) + + ####################################################################### + # Door Detection Input + ####################################################################### + self.input_topic_points = rospy.get_param('~input_topic_points') + + ####################################################################### + # ROS Clients + ####################################################################### + global_costmap_inflation_plugin = rospy.get_param( + '~global_costmap_inflation_plugin', + '/move_base/global_costmap/inflation_layer' + ) + local_costmap_inflation_plugin = rospy.get_param( + '~local_costmap_inflation_plugin', + '/move_base/local_costmap/inflation_layer' + ) + self.client_global_inflater = dynamic_reconfigure.client.Client(global_costmap_inflation_plugin) + self.client_local_inflater = dynamic_reconfigure.client.Client(local_costmap_inflation_plugin) + self.switchbot_ros_client = SwitchBotROSClient() + self.switchbot_ros_client.action_client.wait_for_server(timeout=rospy.Duration(10)) + self.look_at_client = rospy.ServiceProxy('~look_at', LookAtTarget) + self.reset_movement = rospy.ServiceProxy('~reset_movement', Trigger) + self.move_base_client = actionlib.SimpleActionClient('/move_base', MoveBaseAction) + rospy.logwarn('create ROS client') + + ####################################################################### + # Get Default Inflation Radius + ####################################################################### + self.update_default_inflation_radius() + rospy.logwarn('get inflation radius') + + ####################################################################### + # Elevator State and Subscribers + ####################################################################### + self.state_door_state = DoorState.UNKNOWN + self.state_elevator_movement = None + self.state_current_floor = None + self.subscriber_door_state = rospy.Subscriber( + '~state_door_state', + DoorState, + self.callback_door_state) + self.subscriber_elevator_movement = rospy.Subscriber( + '~state_elevator_movement', + String, + self.callback_elevator_movement) + self.subscriber_current_floor = rospy.Subscriber( + '~state_current_floor', + Int16, + self.callback_current_floor) + rate = rospy.Rate(1) + while not rospy.is_shutdown() and \ + (self.state_door_state is None or + self.state_elevator_movement is None or + self.state_current_floor is None): + rospy.logwarn('waiting for state messages...') + rospy.logwarn('door: {}, movement: {}, floor: {}'.format( + self.state_door_state, + self.state_elevator_movement, + self.state_current_floor + )) + rate.sleep() + + ####################################################################### + # ROS Action Server + ####################################################################### + self.action_server = actionlib.SimpleActionServer( + '~move_floor_with_elevator', + MoveFloorWithElevatorAction, + self.execute_cb, + auto_start=False + ) + self.action_server.start() + + rospy.loginfo('initialized') + + def callback_door_state(self, msg): + self.state_door_state = msg.state + + def callback_elevator_movement(self, msg): + self.state_elevator_movement = msg.data + + def callback_current_floor(self, msg): + self.state_current_floor = msg.data + + def get_inflation_radius(self): + + cfg_global = self.client_global_inflater.get_configuration() + cfg_local = self.client_local_inflater.get_configuration() + return cfg_global['inflation_radius'], cfg_local['inflation_radius'] + + def set_inflation_radius(self, global_inflation_radius, local_inflation_radius): + + self.client_global_inflater.update_configuration( + {'inflation_radius': global_inflation_radius}) + self.client_local_inflater.update_configuration( + {'inflation_radius': local_inflation_radius}) + + def update_default_inflation_radius(self): + + global_inflation_radius, local_inflation_radius = self.get_inflation_radius() + self.default_global_inflation_radius = global_inflation_radius + self.default_local_inflation_radius = local_inflation_radius + + def recover_default_inflation_radius(self): + + self.set_inflation_radius( + self.default_global_inflation_radius, + self.default_local_inflation_radius + ) + + def start_door_detector(self, + input_topic_points, + elevator_door_frame_id, + door_dimension_x, + door_dimension_y, + door_dimension_z, + door_position_offset='[0,0,0]', + door_rotation_offset='[0,0,0]', + duration_timeout=10): + + if self.roslaunch_parent is not None: + self.stop_door_detector() + uuid = roslaunch.rlutil.get_or_generate_uuid(None, True) + roslaunch_path = rospkg.RosPack().get_path('elevator_operation') + '/launch/elevator_door_detector.launch' + cli_args = [roslaunch_path, + 'input_topic_points:={}'.format(input_topic_points), + 'elevator_door_frame_id:={}'.format(elevator_door_frame_id), + 'door_position_offset:={}'.format(door_position_offset), + 'door_rotation_offset:={}'.format(door_rotation_offset), + 'door_dimension_x:={}'.format(door_dimension_x), + 'door_dimension_y:={}'.format(door_dimension_y), + 'door_dimension_z:={}'.format(door_dimension_z), + ] + roslaunch_file = [( + roslaunch.rlutil.resolve_launch_arguments(cli_args)[0], + cli_args[1:] + )] + rospy.logwarn('roslaunch_file: {}'.format(roslaunch_file)) + self.roslaunch_parent = roslaunch.parent.ROSLaunchParent( + uuid, roslaunch_file + ) + self.roslaunch_parent.start() + try: + rospy.wait_for_message( + '/elevator_door_detector/door_state', + DoorState, + timeout=duration_timeout) + rospy.loginfo('Door detector started') + return True + except (rospy.ROSException, rospy.ROSInterruptException) as e: + rospy.logerr('{}'.format(e)) + self.stop_door_detector() + return False + + def stop_door_detector(self): + + if self.roslaunch_parent is not None: + self.roslaunch_parent.shutdown() + self.roslaunch_parent = None + + def _move_to(self, target_pose, wait=False): + + frame_id = target_pose['frame_id'] + position = target_pose['position'] + orientation = target_pose['orientation'] + + goal = MoveBaseGoal() + goal.target_pose.header.stamp = rospy.Time.now() + goal.target_pose.header.frame_id = frame_id + goal.target_pose.pose.position.x = position[0] + goal.target_pose.pose.position.y = position[1] + goal.target_pose.pose.position.z = position[2] + goal.target_pose.pose.orientation.x = orientation[0] + goal.target_pose.pose.orientation.y = orientation[1] + goal.target_pose.pose.orientation.z = orientation[2] + goal.target_pose.pose.orientation.w = orientation[3] + self.move_base_client.send_goal(goal) + if wait: + self.move_base_client.wait_for_result() + + def execute_cb(self, goal): + rospy.loginfo('action started') + result = MoveFloorWithElevatorResult() + if goal.target_floor_name not in [v['floor_name'] if 'floor_name' in v else None for k, v in self.elevator_config.items()]: + rospy.logerr('target_floor: {} not in elevator_config'.format(goal.target_floor_name)) + result.success = False + self.action_server.set_aborted(result) + else: + target_floor = filter( + lambda v: 'floor_name' in v and v['floor_name'] == goal.target_floor_name, + self.elevator_config.values() + )[0]['floor'] + ret = self.move_floor_with_elevator(target_floor) + result.success = ret + self.action_server.set_succeeded(result) + rospy.loginfo('action finished') + + def move_floor_with_elevator(self, target_floor): + + start_floor = self.state_current_floor + + # move robot to the front of elevator + self._move_to( + self.elevator_config[start_floor]['outside_pose'], + wait=True + ) + rospy.loginfo('moved to the front of elevator') + + # set inflation_radius + self.update_default_inflation_radius() + self.set_inflation_radius(0.2, 0.2) + + # look to the door + self.look_at_client(self.elevator_config[start_floor]['door_frame_id']) + rospy.loginfo('look at elevator') + + # Start door detection + ret = self.start_door_detector( + self.input_topic_points, + self.elevator_config[start_floor]['door_frame_id'], + self.elevator_config[start_floor]['door_dimensions'][0], + self.elevator_config[start_floor]['door_dimensions'][1], + self.elevator_config[start_floor]['door_dimensions'][2], + ) + rospy.loginfo('result of door detector: {}'.format(ret)) + if not ret: + self.recover_default_inflation_radius() + return False + + # Get button in current floor and target floor + if target_floor > start_floor: + button_from = self.elevator_config[start_floor]['buttons']['up'] + button_to = self.elevator_config[target_floor]['buttons']['down'] + else: + button_from = self.elevator_config[start_floor]['buttons']['down'] + button_to = self.elevator_config[target_floor]['buttons']['up'] + + # Call elevator + rospy.loginfo('Calling switchbot device: {} action: press'.format(button_from)) + ret = self.switchbot_ros_client.control_device( + button_from, + 'press', + wait=True + ) + rospy.loginfo('Call elevator from start floor: {}'.format(ret)) + + # Wait until arrive + rate = rospy.Rate(1) + while not rospy.is_shutdown(): + rate.sleep() + rospy.loginfo('waiting for door to open.') + rospy.loginfo('door_state: {}'.format(self.state_door_state)) + if self.state_door_state == DoorState.OPEN: + break + + # Ride on when arrive + self._move_to( + self.elevator_config[start_floor]['inside_pose'] + ) + # Call elevator from target floor + self.switchbot_ros_client.control_device( + button_to, + 'press', + wait=True + ) + # press current floor button until riding on + rate = rospy.Rate(0.2) + while not rospy.is_shutdown(): + # press button again + ret = self.switchbot_ros_client.control_device( + button_from, + 'press', + wait=True + ) + rate.sleep() + if self.move_base_client.wait_for_result(timeout=rospy.Duration(1)): + break + + # reset movement + self.reset_movement() + + # look to the door + self.look_at_client(self.elevator_config[start_floor]['door_frame_id']) + + # Wait until arrive + rate = rospy.Rate(1) + while not rospy.is_shutdown(): + rate.sleep() + rospy.loginfo('door: {}, floor: {}, movement: {}'.format( + self.state_door_state, + self.state_current_floor, + self.state_elevator_movement + )) + if self.state_door_state == DoorState.OPEN \ + and self.state_current_floor == target_floor \ + and self.state_elevator_movement == 'halt': + break + + # Get off when arrive + self._move_to( + self.elevator_config[target_floor]['outside_pose'], + ) + rate = rospy.Rate(0.2) + while not rospy.is_shutdown(): + # press button again + self.switchbot_ros_client.control_device( + button_to, + 'press', + wait=True + ) + rate.sleep() + if self.move_base_client.wait_for_result(timeout=rospy.Duration(1)): + break + + # stop door detector + self.stop_door_detector() + + # recover inflation radius + self.recover_default_inflation_radius() + + rospy.loginfo('Finished.') + return True + + +if __name__ == '__main__': + + rospy.init_node('elevator_operation_server') + node = ElevatorOperationServer() + rospy.spin() diff --git a/elevator_operation/node_scripts/elevator_state_publisher.py b/elevator_operation/node_scripts/elevator_state_publisher.py new file mode 100755 index 0000000000..1d3e859bec --- /dev/null +++ b/elevator_operation/node_scripts/elevator_state_publisher.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python + +import math + +import rospy + +from std_msgs.msg import String +from std_msgs.msg import Bool +from std_msgs.msg import Int16 +from std_msgs.msg import Float32 +from std_srvs.srv import Trigger, TriggerResponse + + +class ElevatorStatePublisher(object): + + def __init__(self): + + # Publishers and Subscribers + self.pub_current_floor = rospy.Publisher('~current_floor', Int16, queue_size=1) + self.pub_elevator_movement = rospy.Publisher('~elevator_movement', String, queue_size=1) + self.pub_rest_elevator = rospy.Publisher('~rest_elevator', Bool, queue_size=1) + + self.pub_change_floor = rospy.Publisher('~change_floor', String, queue_size=1) + self._start_subscriber() + + # Parameters + elevator_config = rospy.get_param('~elevator_config', []) + initial_floor = rospy.get_param('~initial_floor', 7) + threshold_altitude = rospy.get_param('~threshold_altitude', 2) + threshold_accel = rospy.get_param('~threshold_accel', 0.2) + + self.elevator_config = {entry['floor']: entry for entry in elevator_config} + + self.state_current_floor = initial_floor + self.state_elevator_movement = 'halt' + self.state_time_elevator_movement_changed = rospy.Time.now() + self.prestate_elevator_movement = 'halt' + + self.param_threshold_altitude = threshold_altitude + self.param_threshold_accel = threshold_accel + self.param_anchor_floor = initial_floor + self.param_anchor_altitude = self.state_altitude + self.param_anchor_time = rospy.Time.now() + self.param_stable_accel = self.state_acc + + # Service + self.srv_reset_movement = rospy.Service('~reset_movement', Trigger, self._handler) + + def spin(self): + + rate = rospy.Rate(10) + while not rospy.is_shutdown(): + rate.sleep() + + # update elevator movement state + acc_diff = self.state_acc - self.param_stable_accel + if self.state_elevator_movement == 'halt': + if acc_diff > self.param_threshold_accel: + self.state_elevator_movement = 'up_accel' + elif acc_diff < - self.param_threshold_accel: + self.state_elevator_movement = 'down_accel' + elif self.state_elevator_movement == 'up_accel': + if acc_diff <= self.param_threshold_accel: + self.state_elevator_movement = 'up_stable' + elif self.state_elevator_movement == 'up_stable': + if acc_diff < - self.param_threshold_accel: + self.state_elevator_movement = 'up_decel' + elif self.state_elevator_movement == 'up_decel': + if acc_diff >= - self.param_threshold_accel: + self.state_elevator_movement = 'halt' + elif self.state_elevator_movement == 'down_accel': + if acc_diff >= - self.param_threshold_accel: + self.state_elevator_movement = 'down_stable' + elif self.state_elevator_movement == 'down_stable': + if acc_diff > self.param_threshold_accel: + self.state_elevator_movement = 'down_decel' + elif self.state_elevator_movement == 'down_decel': + if acc_diff <= self.param_threshold_accel: + self.state_elevator_movement = 'halt' + if self.state_elevator_movement != self.prestate_elevator_movement: + self.state_time_elevator_movement_changed = rospy.Time.now() + + # make elevator movement state to halt when state not changed in given duration + if rospy.Time.now() - self.state_time_elevator_movement_changed > rospy.Duration(30): + self.state_elevator_movement = 'halt' + self.state_time_elevator_movement_changed = rospy.Time.now() + + # update current floor state + altitude_diff_list = [ + { + 'floor': key, + 'altitude_diff': + (entry['altitude'] - self.elevator_config[self.param_anchor_floor]['altitude']) - (self.state_altitude - self.param_anchor_altitude) + } + for key, entry in self.elevator_config.items()] + nearest_entry = min( + altitude_diff_list, + key=lambda x: math.fabs(x['altitude_diff']) + ) + if math.fabs(nearest_entry['altitude_diff']) < self.param_threshold_altitude: + self.state_current_floor = nearest_entry['floor'] + + # change floor if state is changed to halt + if self.prestate_elevator_movement != 'halt' and \ + self.state_elevator_movement == 'halt': + self.pub_change_floor.publish(String(data=self.elevator_config[self.state_current_floor]['map_name'])) + + # update anchor if state is changed to halt + if self.prestate_elevator_movement != 'halt' and \ + self.state_elevator_movement == 'halt': + self._update_anchor_floor_and_altitude() + + # update anchor if altitude is not changed + if rospy.Time.now() - self.param_anchor_time > rospy.Duration(5) and \ + math.fabs(self.state_altitude - self.param_anchor_altitude) < self.param_threshold_altitude: + self._update_anchor_floor_and_altitude() + + # echo and publish + rospy.loginfo('=====================') + rospy.loginfo('current_floor: {}'.format(self.state_current_floor)) + rospy.loginfo('elevator_movement: {}'.format(self.state_elevator_movement)) + self.pub_current_floor.publish(Int16(data=self.state_current_floor)) + self.pub_elevator_movement.publish(String(data=self.state_elevator_movement)) + if self.state_current_floor == 'halt': + self.pub_rest_elevator.publish(Bool(data=True)) + else: + self.pub_rest_elevator.publish(Bool(data=False)) + + # store state to prestate + self.prestate_elevator_movement = self.state_elevator_movement + + def _update_anchor_floor_and_altitude(self): + + self.param_anchor_floor = self.state_current_floor + self.param_anchor_altitude = self.state_altitude + self.param_anchor_time = rospy.Time.now() + + def _start_subscriber(self): + + msg_altitude = rospy.wait_for_message( + '~input_altitude', + Float32 + ) + msg_acc = rospy.wait_for_message( + '~input_accel', + Float32 + ) + + self.state_altitude = msg_altitude.data + self.state_acc = msg_acc.data + + self.subscriber_altitude = rospy.Subscriber( + '~input_altitude', + Float32, + self._callback_altitude) + self.subscriber_imu = rospy.Subscriber( + '~input_accel', + Float32, + self._callback_imu) + + rate = rospy.Rate(1) + while not rospy.is_shutdown() \ + and self.state_altitude is None \ + and self.state_acc is None: + rospy.loginfo('waiting for message...') + rate.sleep() + + def _callback_altitude(self, msg): + + self.state_altitude = msg.data + + def _callback_imu(self, msg): + + self.state_acc = msg.data + + def _handler(self, req): + + self.state_elevator_movement = 'halt' + if self.state_elevator_movement != self.prestate_elevator_movement: + self.state_time_elevator_movement_changed = rospy.Time.now() + + # change floor if state is changed to halt + if self.prestate_elevator_movement != 'halt' and \ + self.state_elevator_movement == 'halt': + self.pub_change_floor.publish(String(data=self.elevator_config[self.state_current_floor]['map_name'])) + + # update anchor if state is changed to halt + if self.prestate_elevator_movement != 'halt' and \ + self.state_elevator_movement == 'halt': + self._update_anchor_floor_and_altitude() + + return TriggerResponse(success=True) + + +if __name__ == '__main__': + rospy.init_node('elevator_state_publisher') + node = ElevatorStatePublisher() + node.spin() diff --git a/elevator_operation/node_scripts/lowpass_filter.py b/elevator_operation/node_scripts/lowpass_filter.py new file mode 100755 index 0000000000..864311c013 --- /dev/null +++ b/elevator_operation/node_scripts/lowpass_filter.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python + +import rospy +from std_msgs.msg import Float32 + + +class LowpassFilterNode(object): + + # + # See https://qiita.com/yuji0001/items/b0bf121fb8b912c02856 + # + + def __init__(self): + + self.filter_order = rospy.get_param('~filter_order', 3) + self.freq_cutoff = rospy.get_param('~freq_cutoff', 0.1) + self.buffer_length = rospy.get_param('~buffer_length', 100) + self.buffer = list() + self.buffer_stamp = list() + self.pre_time = None + self.pre_acc_z = None + self.initialized = False + self.pub = rospy.Publisher('~output', Float32, queue_size=1) + self.sub = rospy.Subscriber('~input', Float32, self.callback) + + def callback(self, msg): + + current_stamp = rospy.Time.now() + if self.pre_acc_z is None or self.pre_time is None: + self.pre_acc_z = msg.data + self.pre_time = current_stamp + else: + dt = (current_stamp - self.pre_time).to_sec() + fs = 1.0 / dt + rospy.loginfo('fs: {}'.format(fs)) + self.pre_acc_z = (1 - (self.freq_cutoff / fs)) * self.pre_acc_z \ + + (self.freq_cutoff / fs) * msg.data + self.pre_time = current_stamp + + self.pub.publish(Float32(data=self.pre_acc_z)) + + +if __name__ == '__main__': + + rospy.init_node('lowpass_filter') + node = LowpassFilterNode() + rospy.spin() diff --git a/elevator_operation/package.xml b/elevator_operation/package.xml new file mode 100644 index 0000000000..8e4f6229bb --- /dev/null +++ b/elevator_operation/package.xml @@ -0,0 +1,26 @@ + + + elevator_operation + 0.0.0 + The elevator_operation package + + sktometometo + + BSD + + catkin + + actionlib + actionlib_msgs + + actionlib + actionlib_msgs + jsk_pcl_ros + move_base_msgs + rosserial_python + switchbot_ros + jsk_maps + tf2_ros + + + diff --git a/elevator_operation/srv/LookAtTarget.srv b/elevator_operation/srv/LookAtTarget.srv new file mode 100644 index 0000000000..a70860c362 --- /dev/null +++ b/elevator_operation/srv/LookAtTarget.srv @@ -0,0 +1,4 @@ +string frame_id +--- +bool success +string message