#!/usr/bin/env python

"""ackermann_controller.py

Control the wheels of a vehicle with Ackermann steering.

Subscribed Topics:
    ackermann_cmd (ackermann_msgs/AckermannDriveStamped)
        Ackermann command. It contains the vehicle's desired speed and steering
        angle.

Published Topics:
    <left steering controller name>/command (std_msgs/Float64)
        Command for the left steering controller.
    <right steering controller name>/command (std_msgs/Float64)
        Command for the right steering controller.
    <left front axle controller name>/command (std_msgs/Float64)
        Command for the left front axle controller.
    <right front axle controller name>/command (std_msgs/Float64)
        Command for the right front axle controller.
    <left rear axle controller name>/command (std_msgs/Float64)
        Command for the left rear axle controller.
    <right rear axle controller name>/command (std_msgs/Float64)
        Command for the right rear axle controller.
    <shock absorber controller name>/command (std_msgs/Float64)
        One of these topics exists for each shock absorber. They are latched
        topics.

Services Called:
    controller_manager/list_controllers (controller_manager_msgs/
                                         ListControllers)
        List the states of the controllers.

Parameters:
    ~left_front_wheel/steering_link_name (string, default: left_steering_link)
    ~right_front_wheel/steering_link_name (string,
                                           default: right_steering_link)
        Names of links that have origins coincident with the origins of the
        left and right steering joints, respectively. The steering links are
        used to compute the distance between the steering joints, as well as
        the vehicle's wheelbase.

    ~left_front_wheel/steering_controller_name (string, default:
                                                left_steering_controller)
    ~right_front_wheel/steering_controller_name (string, default:
                                                 right_steering_controller)
        Steering controller names.

    ~left_rear_wheel/link_name (string, default: left_wheel)
    ~right_rear_wheel/link_name (string, default: right_wheel)
        Names of links that have origins coincident with the centers of the
        left and right wheels, respectively. The rear wheel links are used to
        compute the vehicle's wheelbase.

    ~left_front_wheel/axle_controller_name (string)
    ~right_front_wheel/axle_controller_name
    ~left_rear_wheel/axle_controller_name
    ~right_rear_wheel/axle_controller_name
        Axle controller names. If no controller name is specified for an axle,
        that axle will not have a controller. This allows the control of
        front-wheel, rear-wheel, and four-wheel drive vehicles.

    ~left_front_wheel/diameter (float, default: 1.0)
    ~right_front_wheel/diameter
    ~left_rear_wheel/diameter
    ~right_rear_wheel/diameter
        Wheel diameters. Each diameter must be greater than zero. Unit: meter.

    ~shock_absorbers (sequence of mappings, default: empty)
        Zero or more shock absorbers.

        Key-Value Pairs:

        controller_name (string)
            Controller name.
        equilibrium_position (float, default: 0.0)
            Equilibrium position. Unit: meter.

    ~cmd_timeout (float, default: 0.5)
        If ~cmd_timeout is greater than zero and this node does not receive a
        command for more than ~cmd_timeout seconds, vehicle motion is paused
        until a command is received. If ~cmd_timeout is less than or equal to
        zero, the command timeout is disabled.
    ~publishing_frequency (float, default: 30.0)
        Joint command publishing frequency. It must be greater than zero.
        Unit: hertz.

Required tf Transforms:
    <~left_front_wheel/steering_link_name> to <~right_rear_wheel/link_name>
        Specifies the position of the left front wheel's steering link in the
        right rear wheel's frame.
    <~right_front_wheel/steering_link_name> to <~right_rear_wheel/link_name>
        Specifies the position of the right front wheel's steering link in the
        right rear wheel's frame.
    <~left_rear_wheel/link_name> to <~right_rear_wheel/link_name>
        Specifies the position of the left rear wheel in the right rear
        wheel's frame.

Copyright (c) 2013 Wunderkammer Laboratory

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

import math
import numpy
import threading

from math import pi

import rospy
import tf

from ackermann_msgs.msg import AckermannDriveStamped
from std_msgs.msg import Float64
from controller_manager_msgs.srv import ListControllers


class _AckermannCtrlr(object):
    """Ackermann controller

    An object of class _AckermannCtrlr is a node that controls the wheels of a
    vehicle with Ackermann steering.
    """

    def __init__(self):
        """Initialize this _AckermannCtrlr."""

        rospy.init_node("ackermann_controller")

        # Parameters

        # Wheels
        (left_steer_link_name, left_steer_ctrlr_name,
         left_front_axle_ctrlr_name, self._left_front_inv_circ) = \
         self._get_front_wheel_params("left")
        (right_steer_link_name, right_steer_ctrlr_name,
         right_front_axle_ctrlr_name, self._right_front_inv_circ) = \
         self._get_front_wheel_params("right")
        (left_rear_link_name, left_rear_axle_ctrlr_name,
         self._left_rear_inv_circ) = \
         self._get_rear_wheel_params("left")
        (self._right_rear_link_name, right_rear_axle_ctrlr_name,
         self._right_rear_inv_circ) = \
         self._get_rear_wheel_params("right")

        list_ctrlrs = rospy.ServiceProxy("controller_manager/list_controllers",
                                         ListControllers)
        list_ctrlrs.wait_for_service()

        # Shock absorbers
        shock_param_list = rospy.get_param("~shock_absorbers", [])
        self._shock_pubs = []
        try:
            for shock_params in shock_param_list:
                try:
                    ctrlr_name = shock_params["controller_name"]
                    try:
                        eq_pos = shock_params["equilibrium_position"]
                    except:
                        eq_pos = self._DEF_EQ_POS
                    eq_pos = float(eq_pos)
                except:
                    rospy.logwarn("An invalid parameter was specified for a "
                                  "shock absorber. The shock absorber will "
                                  "not be used.")
                    continue

                pub = rospy.Publisher(ctrlr_name + "/command", Float64,
                                      latch=True)
                _wait_for_ctrlr(list_ctrlrs, ctrlr_name)
                pub.publish(eq_pos)
                self._shock_pubs.append(pub)
        except:
            rospy.logwarn("The specified list of shock absorbers is invalid. "
                          "No shock absorbers will be used.")

        # Command timeout
        try:
            self._cmd_timeout = float(rospy.get_param("~cmd_timeout",
                                                      self._DEF_CMD_TIMEOUT))
        except:
            rospy.logwarn("The specified command timeout value is invalid. "
                          "The default timeout value will be used instead.")
            self._cmd_timeout = self._DEF_CMD_TIMEOUT

        # Publishing frequency
        try:
            pub_freq = float(rospy.get_param("~publishing_frequency",
                                             self._DEF_PUB_FREQ))
            if pub_freq <= 0.0:
                raise ValueError()
        except:
            rospy.logwarn("The specified publishing frequency is invalid. "
                          "The default frequency will be used instead.")
            pub_freq = self._DEF_PUB_FREQ
        self._sleep_timer = rospy.Rate(pub_freq)

        # _last_cmd_time is the time at which the most recent Ackermann
        # driving command was received.
        self._last_cmd_time = rospy.get_time()

        # _ackermann_cmd_lock is used to control access to _steer_ang,
        # _steer_ang_vel, _speed, _accel, and _jerk.
        self._ackermann_cmd_lock = threading.Lock()
        self._steer_ang = 0.0      # Steering angle
        self._steer_ang_vel = 0.0  # Steering angle velocity
        self._speed = 0.0
        self._accel = 0.0          # Acceleration
        self._jerk = 0.0

        self._last_steer_ang = 0.0  # Last steering angle
        self._theta_left = 0.0      # Left steering joint angle
        self._theta_right = 0.0     # Right steering joint angle

        self._last_speed = 0.0
        self._last_accel_limit = 0.0  # Last acceleration limit
        # Axle angular velocities
        self._left_front_ang_vel = 0.0
        self._right_front_ang_vel = 0.0
        self._left_rear_ang_vel = 0.0
        self._right_rear_ang_vel = 0.0

        # _joint_dist_div_2 is the distance between the steering joints,
        # divided by two.
        tfl = tf.TransformListener()
        ls_pos = self._get_link_pos(tfl, left_steer_link_name)
        rs_pos = self._get_link_pos(tfl, right_steer_link_name)
        self._joint_dist_div_2 = numpy.linalg.norm(ls_pos - rs_pos) / 2
        lrw_pos = self._get_link_pos(tfl, left_rear_link_name)
        rrw_pos = numpy.array([0.0] * 3)
        front_cent_pos = (ls_pos + rs_pos) / 2     # Front center position
        rear_cent_pos = (lrw_pos + rrw_pos) / 2    # Rear center position
        self._wheelbase = numpy.linalg.norm(front_cent_pos - rear_cent_pos)
        self._inv_wheelbase = 1 / self._wheelbase  # Inverse of _wheelbase
        self._wheelbase_sqr = self._wheelbase ** 2

        # Publishers and subscribers

        self._left_steer_cmd_pub = \
            _create_cmd_pub(list_ctrlrs, left_steer_ctrlr_name)
        self._right_steer_cmd_pub = \
            _create_cmd_pub(list_ctrlrs, right_steer_ctrlr_name)

        self._left_front_axle_cmd_pub = \
            _create_axle_cmd_pub(list_ctrlrs, left_front_axle_ctrlr_name)
        self._right_front_axle_cmd_pub = \
            _create_axle_cmd_pub(list_ctrlrs, right_front_axle_ctrlr_name)
        self._left_rear_axle_cmd_pub = \
            _create_axle_cmd_pub(list_ctrlrs, left_rear_axle_ctrlr_name)
        self._right_rear_axle_cmd_pub = \
            _create_axle_cmd_pub(list_ctrlrs, right_rear_axle_ctrlr_name)

        self._ackermann_cmd_sub = \
            rospy.Subscriber("ackermann_cmd", AckermannDriveStamped,
                             self.ackermann_cmd_cb, queue_size=1)

    def spin(self):
        """Control the vehicle."""

        last_time = rospy.get_time()

        while not rospy.is_shutdown():
            t = rospy.get_time()
            delta_t = t - last_time
            last_time = t

            if (self._cmd_timeout > 0.0 and
                t - self._last_cmd_time > self._cmd_timeout):
                # Too much time has elapsed since the last command. Stop the
                # vehicle.
                steer_ang_changed, center_y = \
                    self._ctrl_steering(self._last_steer_ang, 0.0, 0.001)
                self._ctrl_axles(0.0, 0.0, 0.0, 0.001, steer_ang_changed,
                                 center_y)
            elif delta_t > 0.0:
                with self._ackermann_cmd_lock:
                    steer_ang = self._steer_ang
                    steer_ang_vel = self._steer_ang_vel
                    speed = self._speed
                    accel = self._accel
                    jerk = self._jerk
                steer_ang_changed, center_y = \
                    self._ctrl_steering(steer_ang, steer_ang_vel, delta_t)
                self._ctrl_axles(speed, accel, jerk, delta_t,
                                 steer_ang_changed, center_y)

            # Publish the steering and axle joint commands.
            self._left_steer_cmd_pub.publish(self._theta_left)
            self._right_steer_cmd_pub.publish(self._theta_right)
            if self._left_front_axle_cmd_pub:
                self._left_front_axle_cmd_pub.publish(self._left_front_ang_vel)
            if self._right_front_axle_cmd_pub:
                self._right_front_axle_cmd_pub.\
                    publish(self._right_front_ang_vel)
            if self._left_rear_axle_cmd_pub:
                self._left_rear_axle_cmd_pub.publish(self._left_rear_ang_vel)
            if self._right_rear_axle_cmd_pub:
                self._right_rear_axle_cmd_pub.publish(self._right_rear_ang_vel)

            self._sleep_timer.sleep()

    def ackermann_cmd_cb(self, ackermann_cmd):
        """Ackermann driving command callback

        :Parameters:
          ackermann_cmd : ackermann_msgs.msg.AckermannDriveStamped
            Ackermann driving command.
        """
        self._last_cmd_time = rospy.get_time()
        with self._ackermann_cmd_lock:
            self._steer_ang = ackermann_cmd.drive.steering_angle
            self._steer_ang_vel = ackermann_cmd.drive.steering_angle_velocity
            self._speed = ackermann_cmd.drive.speed
            self._accel = ackermann_cmd.drive.acceleration
            self._jerk = ackermann_cmd.drive.jerk

    def _get_front_wheel_params(self, side):
        # Get front wheel parameters. Return a tuple containing the steering
        # link name, steering controller name, axle controller name (or None),
        # and inverse of the circumference.

        prefix = "~" + side + "_front_wheel/"
        steer_link_name = rospy.get_param(prefix + "steering_link_name",
                                          side + "_steering_link")
        steer_ctrlr_name = rospy.get_param(prefix + "steering_controller_name",
                                           side + "_steering_controller")
        axle_ctrlr_name, inv_circ = self._get_common_wheel_params(prefix)
        return steer_link_name, steer_ctrlr_name, axle_ctrlr_name, inv_circ

    def _get_rear_wheel_params(self, side):
        # Get rear wheel parameters. Return a tuple containing the link name,
        # axle controller name, and inverse of the circumference.

        prefix = "~" + side + "_rear_wheel/"
        link_name = rospy.get_param(prefix + "link_name", side + "_wheel")
        axle_ctrlr_name, inv_circ = self._get_common_wheel_params(prefix)
        return link_name, axle_ctrlr_name, inv_circ

    def _get_common_wheel_params(self, prefix):
        # Get parameters used by the front and rear wheels. Return a tuple
        # containing the axle controller name (or None) and the inverse of the
        # circumference.

        axle_ctrlr_name = rospy.get_param(prefix + "axle_controller_name",
                                          None)

        try:
            dia = float(rospy.get_param(prefix + "diameter",
                                        self._DEF_WHEEL_DIA))
            if dia <= 0.0:
                raise ValueError()
        except:
            rospy.logwarn("The specified wheel diameter is invalid. "
                          "The default diameter will be used instead.")
            dia = self._DEF_WHEEL_DIA

        return axle_ctrlr_name, 1 / (pi * dia)

    def _get_link_pos(self, tfl, link):
        # Return the position of the specified link, relative to the right
        # rear wheel link.

        while True:
            try:
                trans, not_used = \
                    tfl.lookupTransform(self._right_rear_link_name, link, 
                                        rospy.Time(0))
                return numpy.array(trans)
            except:
                pass

    def _ctrl_steering(self, steer_ang, steer_ang_vel_limit, delta_t):
        # Control the steering joints.

        # Compute theta, the virtual front wheel's desired steering angle.
        if steer_ang_vel_limit > 0.0:
            # Limit the steering velocity.
            ang_vel = (steer_ang - self._last_steer_ang) / delta_t
            ang_vel = max(-steer_ang_vel_limit,
                           min(ang_vel, steer_ang_vel_limit))
            theta = self._last_steer_ang + ang_vel * delta_t
        else:
            theta = steer_ang

        # Compute the desired steering angles for the left and right front
        # wheels.
        center_y = self._wheelbase * math.tan((pi / 2) - theta)
        steer_ang_changed = theta != self._last_steer_ang
        if steer_ang_changed:
            self._last_steer_ang = theta
            self._theta_left = \
                _get_steer_ang(math.atan(self._inv_wheelbase *
                                         (center_y - self._joint_dist_div_2)))
            self._theta_right = \
                _get_steer_ang(math.atan(self._inv_wheelbase *
                                         (center_y + self._joint_dist_div_2)))

        return steer_ang_changed, center_y

    def _ctrl_axles(self, speed, accel_limit, jerk_limit, delta_t,
                    steer_ang_changed, center_y):
        # Control the axle joints.

        # Compute veh_speed, the vehicle's desired speed.
        if accel_limit > 0.0:
            # Limit the vehicle's acceleration.

            if jerk_limit > 0.0:
                if self._last_accel_limit > 0.0:
                    jerk = (accel_limit - self._last_accel_limit) / delta_t
                    jerk = max(-jerk_limit, min(jerk, jerk_limit))
                    accel_limit_2 = self._last_accel_limit + jerk * delta_t
                else:
                    accel_limit_2 = accel_limit
            else:
                accel_limit_2 = accel_limit
            self._last_accel_limit = accel_limit_2

            accel = (speed - self._last_speed) / delta_t
            accel = max(-accel_limit_2, min(accel, accel_limit_2))
            veh_speed = self._last_speed + accel * delta_t
        else:
            self._last_accel_limit = accel_limit
            veh_speed = speed

        # Compute the desired angular velocities of the wheels.
        if veh_speed != self._last_speed or steer_ang_changed:
            self._last_speed = veh_speed
            left_dist = center_y - self._joint_dist_div_2
            right_dist = center_y + self._joint_dist_div_2

            # Front
            gain = (2 * pi) * veh_speed / abs(center_y)
            r = math.sqrt(left_dist ** 2 + self._wheelbase_sqr)
            self._left_front_ang_vel = gain * r * self._left_front_inv_circ
            r = math.sqrt(right_dist ** 2 + self._wheelbase_sqr)
            self._right_front_ang_vel = gain * r * self._right_front_inv_circ
            # Rear
            gain = (2 * pi) * veh_speed / center_y
            self._left_rear_ang_vel = \
                gain * left_dist * self._left_rear_inv_circ
            self._right_rear_ang_vel = \
                gain * right_dist * self._right_rear_inv_circ

    _DEF_WHEEL_DIA = 1.0    # Default wheel diameter. Unit: meter.
    _DEF_EQ_POS = 0.0       # Default equilibrium position. Unit: meter.
    _DEF_CMD_TIMEOUT = 0.5  # Default command timeout. Unit: second.
    _DEF_PUB_FREQ = 30.0    # Default publishing frequency. Unit: hertz.
# end _AckermannCtrlr


def _wait_for_ctrlr(list_ctrlrs, ctrlr_name):
    # Wait for the specified controller to be in the "running" state.
    # Commands can be lost if they are published before their controller is
    # running, even if a latched publisher is used.

    while True:
        response = list_ctrlrs()
        for ctrlr in response.controller:
            if ctrlr.name == ctrlr_name:
                if ctrlr.state == "running":
                    return
                rospy.sleep(0.1)
                break


def _create_axle_cmd_pub(list_ctrlrs, axle_ctrlr_name):
    # Create an axle command publisher.
    if not axle_ctrlr_name:
        return None
    return _create_cmd_pub(list_ctrlrs, axle_ctrlr_name)


def _create_cmd_pub(list_ctrlrs, ctrlr_name):
    # Create a command publisher.
    _wait_for_ctrlr(list_ctrlrs, ctrlr_name)
    return rospy.Publisher(ctrlr_name + "/command", Float64)


def _get_steer_ang(phi):
    # Return the desired steering angle for a front wheel.
    if phi >= 0.0:
        return (pi / 2) - phi
    return (-pi / 2) - phi


# main
if __name__ == "__main__":
    ctrlr = _AckermannCtrlr()
    ctrlr.spin()