Example #1
0
 def __init__(self, swarmie_name, app_root_dir):
     self.swarmie_name = swarmie_name
     self.swarmie_id = RlBehavior.NAME_TO_ID_MAP.index(swarmie_name)
     self.emitter = EventEmitter()
     self.mode = RlBehavior.MANUAL_MODE
     self.tf = None
     self.coord_xform = CoordinateTransform(RlBehavior.ARENA_X_RANGE,
                                            RlBehavior.ARENA_Y_RANGE,
                                            RlBehavior.GRID_QUANTIZATION)
     self.arena = Arena(len(RlBehavior.NAME_TO_ID_MAP), self.coord_xform,
                        RlBehavior.NEST_X_TOP_LEFT, RlBehavior.NEST_DIMS,
                        self.emitter)
     self.swarmie_state = SwarmieState(self.swarmie_name, self.swarmie_id,
                                       self.coord_xform, self.emitter)
     self.rl_state_rep = StateRepository(
         list(range(len(RlBehavior.NAME_TO_ID_MAP))), self.swarmie_id,
         RlBehavior.CUBE_COUNT, RlBehavior.GRID_QUANTIZATION, self.emitter)
     self.model_file_path = os.path.join(
         app_root_dir, 'rl_models', '{}_model.h5'.format(self.swarmie_name))
     self.rl_agent = None
     self.tag_state = None
     self._latest_tag_list = None
     self._current_action = None
     self._map_center = None
     self._map_current = None
     # Publishers ---------------------------------------------------------------
     self.status_pub = None
     self.hb_pub = None
     self.infolog_pub = None
     self.drive_cmd_pub = None
     self.wrist_cmd_pub = None
     self.fingers_cmd_pub = None
     # Timers -------------------------------------------------------------------
     self.status_timer = None
     self.timestep_timer = None
     self.hb_timer = None
     # Subscribers --------------------------------------------------------------
     self.map_sub = None
     self.odom_sub = None
     self.mode_sub = None
     self.sonar_left_sub = None
     self.sonar_center_sub = None
     self.sonar_right_sub = None
     self.sonar_sync = None
     self.joy_sub = None
     self.target_sub = None
     self.maint_cmd_sub = None
# the COPYING file for more details.


from events import EventEmitter

if __name__ == '__main__':
    print("""Expected output:
    a2
    b2
    [<function a at ...>, <function b at ...>]
    b3
    3
    []
    []
    """)
    ee = EventEmitter()
    ee.emit('log', 1)
    def a(p):
        print('a' + repr(p))
    def b(p):
        print('b' + repr(p))
    ee.on('log', a)
    ee.on('log', b)
    ee.emit('log', 2)
    print(ee.listeners('log'))
    ee.remove_listener('log', a)
    ee.emit('log', 3)
    ee.on('log', b)
    ee.on('log', b)
    print(EventEmitter.listener_count(ee, 'log'))
    ee.on('gol', b)
# -*- coding: utf-8 -*-

# Script to do some test for events.py
#
# Copyright (c) 2014 Tristan Cavelier <*****@*****.**>
# This program is free software. It comes without any warranty, to
# the extent permitted by applicable law. You can redistribute it
# and/or modify it under the terms of the Do What The F**k You Want
# To Public License, Version 2, as published by Sam Hocevar. See
# the COPYING file for more details.


from events import EventEmitter

if __name__ == '__main__':
    print("""Expected output:
    a
    b
    """)
    ee = EventEmitter()
    def a():
        print('a')
        ee.emit('b')
    def b():
        print('b')
    ee.on('a', a)
    ee.on('b', b)
    ee.emit('a')
Example #4
0
    def real_loc_in_nest(self, real_coords):
        grid_coords = self.coord_xform.from_real_to_grid(real_coords)
        return self.grid_loc_in_nest(grid_coords)

    def grid_loc_in_nest(self, grid_coords):
        return self._nest_mask[grid_coords]

    def grid_to_str(self, grid):
        return '\n'.join(
            map(
                lambda row: ''.join(map(lambda cell: '*'
                                        if cell else '.', row)), grid))


if __name__ == '__main__':
    from coordxform import CoordinateTransform
    from events import EventEmitter
    my_emitter = EventEmitter()
    my_xform = CoordinateTransform((-7.5, 7.5), (-7.5, 7.5), (
        30,
        30,
    ))
    my_arena = Arena(3, my_xform, (-2.0, 2.0), (4.0, 4.0), my_emitter)
    from events import SwarmieLocEvent
    my_arena.swarmie_loc_update(SwarmieLocEvent(0, (-7.5, 7.5)))
    my_arena.swarmie_loc_update(SwarmieLocEvent(1, (-7.5, 6.5)))
    my_arena.swarmie_loc_update(SwarmieLocEvent(2, (-6.9, 7.5)))
    print '{}'.format(my_arena.grid_to_str(my_arena.build_planning_grid()))

# vim: set ts=2 sw=2 expandtab:
Example #5
0
class RlBehavior(object):
    NAME_TO_ID_MAP = ['achilles', 'aeneas', 'ajax']
    HB_PERIOD = rospy.Duration(2, 0)
    STATUS_PERIOD = rospy.Duration(1, 0)
    MANUAL_MODE = 1
    AUTONOMOUS_MODE = 2
    MODE_DICT = {
        MANUAL_MODE: {
            'log_fmt': '{swarmie_name} switching to MANUAL_MODE',
            'infolog_fmt': '{swarmie_name} under manual control',
        },
        AUTONOMOUS_MODE: {
            'log_fmt': '{swarmie_name} switching to AUTONOMOUS_MODE',
            'infolog_fmt': '{swarmie_name} operating autonomously',
        }
    }
    WARMUP_TIME = rospy.Duration(30, 0)
    TIMESTEP_PERIOD = rospy.Duration(0, 100000000)
    JOYSTICK_LINEAR_AXIS = 4
    JOYSTICK_ANGULAR_AXIS = 3
    MAX_MOTOR_CMD = 255.0
    INIT_PLACE_RADIUS = 1.3
    ARENA_X_RANGE = (-7.5, 7.5)
    ARENA_Y_RANGE = (-7.5, 7.5)
    GRID_QUANTIZATION = (30, 30)
    NEST_X_TOP_LEFT = (-0.78, 0.78)
    NEST_DIMS = (1.56, 1.56)
    CUBE_ANNOUNCE_THRESHOLD = 0.25  # meters
    CUBE_COUNT = 16
    ACTION_COUNT = 2
    SEARCH_ACTION = 0
    FETCH_ACTION = 1
    ACTION_NAMES = {SEARCH_ACTION: 'SEARCH', FETCH_ACTION: 'FETCH'}
    ODOM_DRIFT_TOLERANCE = 0.5

    def __init__(self, swarmie_name, app_root_dir):
        self.swarmie_name = swarmie_name
        self.swarmie_id = RlBehavior.NAME_TO_ID_MAP.index(swarmie_name)
        self.emitter = EventEmitter()
        self.mode = RlBehavior.MANUAL_MODE
        self.tf = None
        self.coord_xform = CoordinateTransform(RlBehavior.ARENA_X_RANGE,
                                               RlBehavior.ARENA_Y_RANGE,
                                               RlBehavior.GRID_QUANTIZATION)
        self.arena = Arena(len(RlBehavior.NAME_TO_ID_MAP), self.coord_xform,
                           RlBehavior.NEST_X_TOP_LEFT, RlBehavior.NEST_DIMS,
                           self.emitter)
        self.swarmie_state = SwarmieState(self.swarmie_name, self.swarmie_id,
                                          self.coord_xform, self.emitter)
        self.rl_state_rep = StateRepository(
            list(range(len(RlBehavior.NAME_TO_ID_MAP))), self.swarmie_id,
            RlBehavior.CUBE_COUNT, RlBehavior.GRID_QUANTIZATION, self.emitter)
        self.model_file_path = os.path.join(
            app_root_dir, 'rl_models', '{}_model.h5'.format(self.swarmie_name))
        self.rl_agent = None
        self.tag_state = None
        self._latest_tag_list = None
        self._current_action = None
        self._map_center = None
        self._map_current = None
        # Publishers ---------------------------------------------------------------
        self.status_pub = None
        self.hb_pub = None
        self.infolog_pub = None
        self.drive_cmd_pub = None
        self.wrist_cmd_pub = None
        self.fingers_cmd_pub = None
        # Timers -------------------------------------------------------------------
        self.status_timer = None
        self.timestep_timer = None
        self.hb_timer = None
        # Subscribers --------------------------------------------------------------
        self.map_sub = None
        self.odom_sub = None
        self.mode_sub = None
        self.sonar_left_sub = None
        self.sonar_center_sub = None
        self.sonar_right_sub = None
        self.sonar_sync = None
        self.joy_sub = None
        self.target_sub = None
        self.maint_cmd_sub = None

    def run(self):
        print('Welcome to the world of tomorrow {}!'.format(self.swarmie_name))
        rospy.init_node('{}_BEHAVIOUR'.format(self.swarmie_name))
        self.tf = TransformListener()
        self.tag_state = TagState(self.swarmie_name, self.swarmie_state,
                                  self.tf, self.coord_xform, self.arena)
        self.rl_agent = DqnAgent(policy=GreedyPolicy(),
                                 state_size=self.rl_state_rep.state_size,
                                 action_size=RlBehavior.ACTION_COUNT,
                                 model_file_path=self.model_file_path)
        # --------------------------------------------------------------------------
        # Set up all the publishers
        #
        self.status_pub = rospy.Publisher('{}/swarmie_status'.format(
            self.swarmie_name),
                                          String,
                                          queue_size=10)
        self.hb_pub = rospy.Publisher('{}/behaviour/heartbeat'.format(
            self.swarmie_name),
                                      String,
                                      queue_size=1)
        self.infolog_pub = rospy.Publisher('/infoLog', String, queue_size=1)
        self.drive_cmd_pub = rospy.Publisher('{}/driveControl'.format(
            self.swarmie_name),
                                             Skid,
                                             queue_size=10)
        self.wrist_cmd_pub = rospy.Publisher('{}/wristAngle/cmd'.format(
            self.swarmie_name),
                                             Float32,
                                             queue_size=1)
        self.fingers_cmd_pub = rospy.Publisher('{}/fingerAngle/cmd'.format(
            self.swarmie_name),
                                               Float32,
                                               queue_size=1)
        # --------------------------------------------------------------------------
        # Set up all the subscribers
        #
        self.map_sub = rospy.Subscriber('{}/odom/ekf'.format(
            self.swarmie_name),
                                        Odometry,
                                        callback=self._on_map_update,
                                        queue_size=10)
        self.odom_sub = rospy.Subscriber('{}/odom/filtered'.format(
            self.swarmie_name),
                                         Odometry,
                                         callback=self._on_odom_update,
                                         queue_size=10)
        self.mode_sub = rospy.Subscriber('{}/mode'.format(self.swarmie_name),
                                         UInt8,
                                         callback=self._on_mode_change,
                                         queue_size=10)
        self.sonar_left_sub = message_filters.Subscriber(
            '{}/sonarLeft'.format(self.swarmie_name), Range)
        self.sonar_center_sub = message_filters.Subscriber(
            '{}/sonarCenter'.format(self.swarmie_name), Range)
        self.sonar_right_sub = message_filters.Subscriber(
            '{}/sonarRight'.format(self.swarmie_name), Range)
        self.sonar_sync = message_filters.TimeSynchronizer([
            self.sonar_left_sub,
            self.sonar_center_sub,
            self.sonar_right_sub,
        ],
                                                           queue_size=10)
        self.sonar_sync.registerCallback(self._on_sonar_update)
        self.joy_sub = rospy.Subscriber('{}/joystick'.format(
            self.swarmie_name),
                                        Joy,
                                        callback=self._on_joy_cmd,
                                        queue_size=10)
        self.target_sub = rospy.Subscriber('/{}/targets'.format(
            self.swarmie_name),
                                           AprilTagDetectionArray,
                                           callback=self._on_tags_spotted,
                                           queue_size=10)
        self.maint_cmd_sub = rospy.Subscriber('{}/maintCmd'.format(
            self.swarmie_name),
                                              String,
                                              callback=self._on_maint_cmd,
                                              queue_size=10)
        # --------------------------------------------------------------------------
        # Set up all the timers
        #
        self.hb_timer = rospy.Timer(RlBehavior.HB_PERIOD, self._on_hb_timer)
        self.status_timer = rospy.Timer(RlBehavior.STATUS_PERIOD,
                                        self._on_status_timer)
        # --------------------------------------------------------------------------
        # Away we go!
        #
        self.rl_state_rep.init_remote()
        rospy.Timer(RlBehavior.WARMUP_TIME, self._on_warmup_timer)
        rospy.spin()

    @property
    def initialized(self):
        return self.swarmie_state.ready

    def _on_hb_timer(self, event):
        hb_msg = String()
        hb_msg.data = ''
        self.hb_pub.publish(hb_msg)

    def _on_status_timer(self, event):
        status_msg = String()
        status_msg.data = 'learners'
        self.status_pub.publish(status_msg)

    def _on_warmup_timer(self, event):
        # Start the timestep evaluation
        if self.timestep_timer is None:
            self.timestep_timer = rospy.Timer(RlBehavior.TIMESTEP_PERIOD,
                                              self._on_timestep_timer)

    def _on_timestep_timer(self, event):
        """
    :param event:
    :type event: rospy.timer.TimerEvent
    """
        time_since_last_step = None
        if event.last_real is not None:
            time_since_last_step = (rospy.Time.now() - event.last_real)
        try:
            # ------------------------------------------------------------------------
            # Initial timestep
            if not self.initialized:
                self.infolog_pub.publish(
                    String(
                        data='{} willing to learn!'.format(self.swarmie_name)))
                self.swarmie_state.initialize_centers(
                    RlBehavior.INIT_PLACE_RADIUS)
                rospy.loginfo(
                    '{} perceived center in odom frame at ({},{})'.format(
                        self.swarmie_name, self.swarmie_state.odom_center[0],
                        self.swarmie_state.odom_center[1]))
                self._map_center = (self._map_current[0] +
                                    (RlBehavior.INIT_PLACE_RADIUS *
                                     math.cos(self._map_current[2])),
                                    self._map_current[1] +
                                    (RlBehavior.INIT_PLACE_RADIUS *
                                     math.sin(self._map_current[2])),
                                    self._map_current[2])
                if not self.initialized:
                    rospy.logfatal(
                        '{} failed initialization with {} {} {}'.format(
                            self.swarmie_name,
                            self.swarmie_state._positions_populated,
                            self.swarmie_state.linear_vel,
                            self.swarmie_state.angular_vel))
                    self.hb_timer.shutdown()
                    self.status_timer.shutdown()
                    self.timestep_timer.shutdown()
                    rospy.signal_shutdown(
                        'Odometry data not available for {}'.format(
                            self.swarmie_name))
            # ------------------------------------------------------------------------
            # Evaluate and mitigate any Odometry drift
            # timestep_step = 'Evaluating Odometry'
            # self._evaluate_odom_drift()

            # ------------------------------------------------------------------------
            # Update tag information with latest message
            if self._latest_tag_list is not None:
                self.tag_state.update(self._latest_tag_list).sort()
            for a_tag_report in self.tag_state.cube_tags:
                if a_tag_report.tag_dist <= RlBehavior.CUBE_ANNOUNCE_THRESHOLD:
                    continue
                self.emitter.emit(
                    CubeSpottedEvent(swarmie_id=self.swarmie_id,
                                     cube_loc=a_tag_report.grid_coords[0:2]))
            # ------------------------------------------------------------------------
            # Automatically select an action if in automatic mode
            if self.mode == RlBehavior.AUTONOMOUS_MODE:
                state_vector = self.rl_state_rep.make_state_vector()
                rl_action_id = self.rl_agent.act(state_vector)
                rospy.loginfo('Automated agent selected action {}'.format(
                    RlBehavior.ACTION_NAMES[rl_action_id]))
                if rl_action_id == RlBehavior.SEARCH_ACTION and not isinstance(
                        self._current_action, SearchAction):
                    if self._current_action is None:
                        # TEMPORARY SETUP
                        SEARCH_QUAD_FOR = {
                            'achilles': SearchAction.ALPHA_QUADRANT,
                            'aeneas': SearchAction.CHARLIE_QUADRANT,
                            'ajax': SearchAction.BRAVO_QUADRANT
                        }
                        quad_name = SEARCH_QUAD_FOR[self.swarmie_name]
                        self._current_action = SearchAction(
                            self.swarmie_name, self.arena, quad_name)
                    else:
                        self._current_action = None
                elif rl_action_id == RlBehavior.FETCH_ACTION and not isinstance(
                        self._current_action, FetchAction):
                    if self._current_action is None and self.rl_state_rep.nearest_cube[
                            self.swarmie_id] is not None:
                        nearest_cube = self.rl_state_rep.nearest_cube[
                            self.swarmie_id]
                        if nearest_cube is not None:
                            self._current_action = FetchAction(
                                self.swarmie_name, self.swarmie_id, self.arena,
                                self.rl_state_rep, self.tag_state)
                    else:
                        self._current_action = None
            # ------------------------------------------------------------------------
            # Execute current action, should there be one.
            action_response = None
            if self._current_action is not None:
                action_response = self._current_action.update(
                    swarmie_state=self.swarmie_state,
                    elapsed_time=time_since_last_step)
            if action_response is not None:
                if action_response.skid:
                    self.drive_cmd_pub.publish(action_response.skid)
                if action_response.wrist:
                    self.wrist_cmd_pub.publish(action_response.wrist)
                if action_response.fingers:
                    self.fingers_cmd_pub.publish(action_response.fingers)
            else:
                self._current_action = None
                self.drive_cmd_pub.publish(Skid(left=0., right=0.))
            # ------------------------------------------------------------------------
            # Age the april tag sightings based on elapsed time
            if time_since_last_step is not None:
                self.tag_state.dock_age(time_since_last_step)
        except Exception:
            # ex_info = sys.exc_info()
            # file_name = '<unknown>'
            # line_num = '<unknown>'
            # func_name = '<unknown>'
            # if len(ex_info) > 2 and ex_info[2] is not None:
            #   tb_info = traceback.extract_tb(ex_info[2], 1)
            #   if len(tb_info) > 0 and len(tb_info[0]) > 3:
            #     (file_name, line_num, func_name, _) = tb_info[0]
            rospy.logwarn('{}'.format(traceback.format_exc()))

    def _evaluate_odom_drift(self):
        map_pose = PoseStamped()
        map_pose.header.stamp = rospy.Time.now()
        map_pose.header.frame_id = '{}/map'.format(self.swarmie_name)
        (map_pose.pose.orientation.x, map_pose.pose.orientation.y,
         map_pose.pose.orientation.z,
         map_pose.pose.orientation.w) = quaternion_from_euler(
             0., 0., self._map_center[2])
        map_pose.pose.position.x = self._map_center[0]
        map_pose.pose.position.y = self._map_center[1]
        odom_pose = None
        try:
            self.tf.waitForTransform(map_pose.header.frame_id,
                                     '{}/odom'.format(self.swarmie_name),
                                     map_pose.header.stamp, rospy.Duration(1))
            odom_pose = self.tf.transformPose(
                '{}/odom'.format(self.swarmie_name), map_pose)
        except tf.Exception as ex:
            rospy.logwarn('Unable to evaluate odometry drift: {}'.format(
                ex.message))
            return
        x_diff = odom_pose.pose.position.x - self.swarmie_state.odom_center[0]
        y_diff = odom_pose.pose.position.y - self.swarmie_state.odom_center[1]
        total_diff = math.hypot(x_diff, y_diff)
        rospy.loginfo('Odometry drift appears to be {}'.format(total_diff))
        if total_diff > RlBehavior.ODOM_DRIFT_TOLERANCE:
            new_center = np.array([
                self.swarmie_state.odom_center[0] + (x_diff / total_diff),
                self.swarmie_state.odom_center[1] + (y_diff / total_diff)
            ])
            rospy.logwarn(
                'Odometry drift out of tolerance by {}. Adjust odom_center from {} to {}'
                .format(total_diff, self.swarmie_state.odom_center[0:2],
                        new_center))

    def _on_odom_update(self, sample):
        """Position update callback.

    Called by ROS whenever a sample arrives via the <swarmie>/odom/filtered
    topic, communicating the currently-sensed position of the swarmie using a
    local (starting point-centered) reference frame.

    :param sample: Sample containing position information.
    :type sample: nav_msgs.msg.Odometry
    """
        (_, _, yaw) = euler_from_quaternion(
            (sample.pose.pose.orientation.x, sample.pose.pose.orientation.y,
             sample.pose.pose.orientation.z, sample.pose.pose.orientation.w))
        self.swarmie_state.odom_current = np.array(
            [sample.pose.pose.position.x, sample.pose.pose.position.y, yaw])
        self.swarmie_state.linear_vel = sample.twist.twist.linear.x
        self.swarmie_state.angular_vel = sample.twist.twist.angular.z

    def _on_map_update(self, sample):
        (_, _, yaw) = euler_from_quaternion(
            (sample.pose.pose.orientation.x, sample.pose.pose.orientation.y,
             sample.pose.pose.orientation.z, sample.pose.pose.orientation.w))
        self._map_current = np.array(
            [sample.pose.pose.position.x, sample.pose.pose.position.y, yaw])

    def _on_mode_change(self, sample):
        """Operating mode update callback.

    Called by ROS whenever a sample arrives via the <swarmie>/mode topic,
    indicating either a mode initialization or a mode change.

    :param sample: UInt8 sample containing the new mode.
    :type sample: std_msgs.msg.UInt8
    """
        if sample.data != self.mode:
            self._current_action = None
            if sample.data in RlBehavior.MODE_DICT:
                self.mode = sample.data
                dict_entry = RlBehavior.MODE_DICT[sample.data]
                rospy.loginfo(dict_entry['log_fmt'].format(
                    swarmie_name=self.swarmie_name))
                self.infolog_pub.publish(dict_entry['infolog_fmt'].format(
                    swarmie_name=self.swarmie_name))
            else:
                rospy.logwarn('{} cannot switch to unknown mode {}'.format(
                    self.swarmie_name, sample.data))

    def _on_sonar_update(self, left_sample, center_sample, right_sample):
        """Sonar data update callback.

    Called by ROS when it is able to deliver a time-homogeneous collection of
    samples from the three (3) sonar sensors in the swarmies.

    :param left_sample: Sample from the left range finder.
    :type left_sample: sensor_msgs.msg.Range
    :param center_sample: Sample from the center range finder.
    :type center_sample: sensor_msgs.msg.Range
    :param right_sample: Sample from the right range finder.
    :type right_sample: sensor_msgs.msg.Range
    """
        rospy.logdebug('{} sonar readings: L:{} C:{} R:{}'.format(
            self.swarmie_name, left_sample.range, center_sample.range,
            right_sample.range))
        self.swarmie_state.sonar_readings = np.array(
            [left_sample.range, center_sample.range, right_sample.range])

    def _on_joy_cmd(self, cmd):
        """Joystick command callback.

    Called by ROS whenever a Joystick command is emitted in the system.

    :param cmd: Joystick command sample.
    :type cmd: sensor_msgs.msg.Joy
    """
        if self.mode == RlBehavior.MANUAL_MODE:
            # ------------------------------------------------------------------------
            # Scale linear motor command, enforcing a +/- 0.1 "dead zone"
            linear_thresh = math.fabs(
                cmd.axes[RlBehavior.JOYSTICK_LINEAR_AXIS]) >= 0.1
            linear_cmd = (cmd.axes[RlBehavior.JOYSTICK_LINEAR_AXIS] *
                          RlBehavior.MAX_MOTOR_CMD)
            linear_cmd = 0.0 if not linear_thresh else linear_cmd
            # ------------------------------------------------------------------------
            # Scale angular motor command, enforcing a +/- 0.1 "dead zone"
            angular_thresh = math.fabs(
                cmd.axes[RlBehavior.JOYSTICK_ANGULAR_AXIS]) >= 0.1
            angular_cmd = (cmd.axes[RlBehavior.JOYSTICK_ANGULAR_AXIS] *
                           RlBehavior.MAX_MOTOR_CMD)
            angular_cmd = 0.0 if not angular_thresh else angular_cmd
            # ------------------------------------------------------------------------
            # Calculate left robot tracks command, restricting the command value to
            # the range [-MAX_MOTOR_CMD, MAX_MOTOR_CMD]
            left_cmd = linear_cmd - angular_cmd
            left_cmd = min(left_cmd, RlBehavior.MAX_MOTOR_CMD)
            left_cmd = max(left_cmd, -1.0 * RlBehavior.MAX_MOTOR_CMD)
            # ------------------------------------------------------------------------
            # Calculate right robot tracks command, restricting the command value to
            # the range [-MAX_MOTOR_CMD, MAX_MOTOR_CMD]
            right_cmd = linear_cmd + angular_cmd
            right_cmd = min(right_cmd, RlBehavior.MAX_MOTOR_CMD)
            right_cmd = max(right_cmd, -1.0 * RlBehavior.MAX_MOTOR_CMD)
            self.drive_cmd_pub.publish(Skid(left=left_cmd, right=right_cmd))

    def _on_tags_spotted(self, tag_list):
        """April tag detection callback.

    Called by ROS whenever the April tag CV task spots tags in images returned
    by the swarmie camera.

    :param tag_list: List of April tags spotted by CV algorithm.
    :type tag_list: apriltags_ros.msg.AprilTagDetectionArray
    """
        self._latest_tag_list = tag_list

    def _on_maint_cmd(self, the_cmd):
        cmd_str = str(the_cmd.data).lower()
        if cmd_str == 'approach':
            self._current_action = ApproachAction(self.swarmie_name,
                                                  self.tag_state)
        elif cmd_str == 'turn_east':
            self._current_action = TurnAction(self.swarmie_name, 0.)
        elif cmd_str == 'turn_west':
            self._current_action = TurnAction(self.swarmie_name, math.pi)
        elif cmd_str == 'turn_north':
            self._current_action = TurnAction(self.swarmie_name,
                                              (math.pi / 2.))
        elif cmd_str == 'turn_south':
            self._current_action = TurnAction(self.swarmie_name,
                                              -1. * (math.pi / 2.))
        elif cmd_str == 'drive_forward':
            self._current_action = DriveAction(self.swarmie_name)
        elif cmd_str == 'drive_backward':
            self._current_action = DriveAction(self.swarmie_name,
                                               forward=False)
        elif cmd_str.startswith('move_to_cell'):
            parsed_cmd = re.search(
                r'move_to_cell\((?P<row>[0-9]+),(?P<col>[0-9]+)\)', cmd_str)
            dest_coords = (
                int(parsed_cmd.group('row')),
                int(parsed_cmd.group('col')),
            )
            self._current_action = MoveToCellAction(self.swarmie_name,
                                                    self.arena, dest_coords)
        elif cmd_str.startswith('move_to_real'):
            parsed_cmd = re.search(
                r'move_to_real\((?P<x>\-?[0-9]+(\.[0-9]+)?),(?P<y>\-?[0-9]+(\.[0-9]+)?)\)',
                cmd_str)
            dest_coords = (
                float(parsed_cmd.group('x')),
                float(parsed_cmd.group('y')),
            )
            self._current_action = MoveToRealAction(self.swarmie_name,
                                                    dest_coords)
        elif cmd_str == 'pickup':
            self._current_action = PickupAction(self.swarmie_name)
        elif cmd_str == 'drop_off':
            self._current_action = DropOffAction(self.swarmie_name)
        elif cmd_str == 'sweep':
            self._current_action = SweepAction(self.swarmie_name,
                                               self.tag_state)
        elif cmd_str.startswith('search'):
            parsed_cmd = re.search(r'search\((?P<quad>[a-z]+)\)', cmd_str)
            quad = parsed_cmd.group('quad')
            self._current_action = SearchAction(self.swarmie_name, self.arena,
                                                quad)
        # elif cmd_str.startswith('fetch'):
        #   parsed_cmd = re.search(r'fetch\((?P<row>[0-9]+),(?P<col>[0-9]+)\)', cmd_str)
        #   dest_coords = (
        #     int(parsed_cmd.group('row')),
        #     int(parsed_cmd.group('col')),
        #   )
        #   self._current_action = FetchAction(self.swarmie_name, self.arena, dest_coords, self.tag_state)
        elif cmd_str == 'nest':
            self._current_action = MoveToRealAction(self.swarmie_name,
                                                    (0.0, 0.0))
        elif cmd_str == 'halt':
            self._current_action = None
        else:
            rospy.loginfo(
                'Did not recognize maintenance command: {}'.format(cmd_str))