Teaching Assistant: Roi Yehoshua roiyeho@gmail.com Agenda • • • • TAO events Allocating sub-plans Defining custom Next protocols Wandering robot example (C)2014 Roi Yehoshua TAO Events • TAO defines an event distribution system using an EventQueue • It’s used for sharing events inside TAO machines • It’s possible to insert external events to the system (from ROS or other custom source) • It is thread safe (C)2014 Roi Yehoshua TAO Event • Each Event is a path containing: – all context names – when this event was created – an event short name at the end • Example: /ContextName/EventName • When you compare two events you can use regular expressions as the name of one event by writing @ at the beggining of the name. – Example: @/Con.*/Event[NT]...e (C)2014 Roi Yehoshua Events Inside TAO • TAO_RAISE(/STOP) - raise global event • TAO_RAISE(STOP) - raise an event related to TAO context • TAO_RAISE(PLAN_NAME/STOP) - raise an event related to TAO PLAN context event • TAO_STOP_CONDITION(event==TAO_EVENT(STOP)) check condition based on an occurred event • TAO_EVENTS_DROP - Clear current events queue (C)2014 Roi Yehoshua Events Outside TAO • Defining an event: – Event e("/STOP") - global / without context – Event e("STOP",call_context) - related to call_context • Raising the event: – events->raiseEvent(e); (C)2014 Roi Yehoshua Events Interface • Event waitEvent() - block function until a new event arrives • Event tryGetEvent(bool& success) - get a new event if exists (non-blocking call) • bool isTerminated() const - check if the event system is closed • void drop_all() - (C)2014 Roi Yehoshua Events Interface Function Event waitEvent() Block function until a new event arrives Event tryGetEvent(bool& success) Get a new event if exists (non-blocking call) bool isTerminated() Check if the event system is closed void drop_all() Clear queue void close() Close event system, releasing all waiting processes spinOne() Send one /SPIN event for validation of STOP conditions spin(double rate_in_hz = 10) Run spinOne() command with rate_in_hz async_spin(double rate_in_hz = 10, double start_delay=0) Run spin command after start_delay without blocking current code execution (C)2014 Roi Yehoshua RosEventQueue • A class derived from decision_making::EventQueue • RosEventQueue creates a connection between ROS (/decision_making/NODE_NAME/events topic) and the internal EventQueue • Must be created after ros::init and ros_decision_making_init (C)2014 Roi Yehoshua Events Example • In the following example, we will add a support for a STOP event to our Incrementer plan • Copy TaskWithStopCondition.cpp to Events.cpp • In the TAO machine definition, add a clause to the STOP condition that checks if a STOP event has occurred (C)2014 Roi Yehoshua TaoEvents.cpp TAO(Incrementer) { TAO_PLANS { Increment } TAO_START_PLAN(Increment); TAO_BGN { TAO_PLAN(Increment) { TAO_START_CONDITION(true); TAO_CALL_TASK(incrementTask); TAO_ALLOCATE_EMPTY TAO_STOP_CONDITION(WM.counter == 100 || event == TAO_EVENT("/STOP")); TAO_NEXT_EMPTY } } TAO_END } (C)2014 Roi Yehoshua Sending Events • Once running, your model will be able to receive events over the decision_making/[NODE_NAME]/events topic • Events to the model can be sent using: $ rostopic pub decision_making/[NODE_NAME]/events std_msgs/String "EVENT_NAME" • For example, to send a STOP event to the tao_events node, type: rostopic pub decision_making/tao_events/events std_msgs/String "STOP" (C)2014 Roi Yehoshua Sending Events Example (C)2014 Roi Yehoshua Allocating SubPlans • We will now change the incrementer plan so it allocates two sub-plans: – Even – this subplan will increment the counter when it has an even value – Odd – this subplan will increment the counter when it has an odd value • Copy BasicPlanWithWM.cpp to AllocatingSubPlans.cpp (C)2014 Roi Yehoshua AllocatingSubPlans.cpp (1) struct WorldModel : public CallContextParameters { int counter; bool even_plan_finished; bool odd_plan_finished; string str() const { stringstream s; s << "counter = " << counter; return s.str(); } }; #define WM TAO_CONTEXT.parameters<WorldModel>() (C)2014 Roi Yehoshua AllocatingSubPlans.cpp (2) TAO_HEADER(Even) TAO_HEADER(Odd) TAO(Incrementer) { TAO_PLANS { Increment } TAO_START_PLAN(Increment); TAO_BGN { TAO_PLAN(Increment) { TAO_START_CONDITION(WM.counter < 100); WM.even_plan_finished = false; WM.odd_plan_finished = false; TAO_ALLOCATE(AllocFirstReady) { TAO_SUBPLAN(Even); TAO_SUBPLAN(Odd); } TAO_STOP_CONDITION(WM.even_plan_finished || WM.odd_plan_finished); TAO_NEXT(NextFirstReady) { TAO_NEXT_PLAN(Increment); } } } TAO_END } (C)2014 Roi Yehoshua AllocatingSubPlans.cpp (3) TAO(Even) { TAO_PLANS { Even, } TAO_START_PLAN(Even); TAO_BGN { TAO_PLAN(Even) { TAO_START_CONDITION(WM.counter % 2 == 0); WM.counter++; cout << "Even: " << WM.str() << endl; boost::this_thread::sleep(boost::posix_time::milliseconds(100)); TAO_ALLOCATE_EMPTY TAO_STOP_CONDITION(true); TAO_NEXT_EMPTY WM.even_plan_finished = true; } } TAO_END } (C)2014 Roi Yehoshua AllocatingSubPlans.cpp (4) TAO(Odd) { TAO_PLANS{ Odd, } TAO_START_PLAN(Odd); TAO_BGN { TAO_PLAN(Odd) { TAO_START_CONDITION(WM.counter % 2 == 1); WM.counter++; cout << "Odd: " << WM.str() << endl; boost::this_thread::sleep(boost::posix_time::milliseconds(100)); TAO_ALLOCATE_EMPTY TAO_STOP_CONDITION(true); TAO_NEXT_EMPTY WM.odd_plan_finished = true; } } TAO_END } (C)2014 Roi Yehoshua SubPlans Behavior • The sub-plans start executing only after the parent plan reaches its stop condition (and not right after allocation) • If the parent’s stop condition is true before any of its sub-plans started running, then the TAO machine ends • If the parent’s stop condition is false and the parent’s plan has a next plan, then it is unpredictable whether the next plan or the sub-plan starts first • Once a plan starts executing, no other plan can execute at the same time (unless using tasks) • The entire TAO machine ends only when the parent plan and all its sub-plans are finished (C)2014 Roi Yehoshua Allocating SubPlans Demo (C)2014 Roi Yehoshua Decision Graph (C)2014 Roi Yehoshua Wandering Robot Plan • Now we will write a TAO plan for a wandering robot • The wandering plan will be composed of two subplans: – Drive – makes the robot drive forward until an obstacle is detected – Turn – makes the robot turn until the way becomes clear • Create a new package called tao_wandering: $ cd ~/dmw/src $ catkin_create_pkg tao_wandering roscpp decision_making decision_making_parser random_numbers sensor_msgs std_msgs geometry_msgs (C)2014 Roi Yehoshua Wandering.cpp (1) #include <iostream> #include #include #include #include <ros/ros.h> <random_numbers/random_numbers.h> <sensor_msgs/LaserScan.h> <geometry_msgs/Twist.h> #include #include #include #include <decision_making/TAO.h> <decision_making/TAOStdProtocols.h> <decision_making/ROSTask.h> <decision_making/DecisionMaking.h> using namespace std; using namespace decision_making; (C)2014 Roi Yehoshua Wandering.cpp (2) /*** Constants ***/ #define MIN_SCAN_ANGLE_RAD -45.0/180*M_PI #define MAX_SCAN_ANGLE_RAD +45.0/180*M_PI #define MIN_DIST_TO_OBSTACLE 0.8 // in meters /*** Variables ***/ random_numbers::RandomNumberGenerator _randomizer; ros::Publisher _velocityPublisher; /*** World model ***/ struct WorldModel : public CallContextParameters { bool obstacleDetected; bool driveFinished; bool turnFinished; string str() const { stringstream s; s << "obstacleDetected = " << obstacleDetected; return s.str(); } }; #define WM TAO_CONTEXT.parameters<WorldModel>() (C)2014 Roi Yehoshua Wandering.cpp (3) /*** TAO machine ***/ TAO_HEADER(Drive) TAO_HEADER(Turn) TAO(Wandering) { TAO_PLANS { Wandering } TAO_START_PLAN(Wandering); TAO_BGN { TAO_PLAN(Wandering) { TAO_START_CONDITION(true); WM.driveFinished = false; WM.turnFinished = false; TAO_ALLOCATE(AllocFirstReady) { TAO_SUBPLAN(Drive); TAO_SUBPLAN(Turn); } TAO_STOP_CONDITION(WM.driveFinished || WM.turnFinished); TAO_NEXT(NextFirstReady) { TAO_NEXT_PLAN(Wandering); } } } TAO_END } (C)2014 Roi Yehoshua Wandering.cpp (4) TAO(Drive) { TAO_PLANS { Drive, } TAO_START_PLAN(Drive); TAO_BGN { TAO_PLAN(Drive) { TAO_START_CONDITION(!WM.obstacleDetected); TAO_CALL_TASK(driveTask); TAO_ALLOCATE_EMPTY TAO_STOP_CONDITION(WM.obstacleDetected); TAO_NEXT_EMPTY WM.driveFinished = true; } } TAO_END } (C)2014 Roi Yehoshua Wandering.cpp (5) TAO(Turn) { TAO_PLANS{ Turn, } TAO_START_PLAN(Turn); TAO_BGN { TAO_PLAN(Turn) { TAO_START_CONDITION(WM.obstacleDetected); TAO_CALL_TASK(turnTask); TAO_ALLOCATE_EMPTY TAO_STOP_CONDITION(true); TAO_NEXT_EMPTY WM.turnFinished = true; } } TAO_END } (C)2014 Roi Yehoshua Wandering.cpp (6) /*** Task implementations ***/ TaskResult driveTask(string name, const CallContext& context, EventQueue& eventQueue) { ROS_INFO("Driving..."); geometry_msgs::Twist forwardMessage; forwardMessage.linear.x = 1.0; // Preemptive wait while (!eventQueue.isTerminated()) { _velocityPublisher.publish(forwardMessage); boost::this_thread::sleep(boost::posix_time::milliseconds(100.0)); } ROS_INFO("Obstacle detected!"); return TaskResult::SUCCESS(); } (C)2014 Roi Yehoshua Wandering.cpp (7) TaskResult turnTask(string name, const CallContext& context, EventQueue& eventQueue) { ROS_INFO("Turning..."); bool turnLeft = _randomizer.uniformInteger(0, 1); geometry_msgs::Twist turnMessage; turnMessage.angular.z = 2 * (turnLeft ? 1 : -1); int timeToTurnMs = _randomizer.uniformInteger(2000, 4000); int turnLoops = timeToTurnMs / 100; for (int i = 0; i < turnLoops; i++) { _velocityPublisher.publish(turnMessage); boost::this_thread::sleep(boost::posix_time::milliseconds(100.0)); } return TaskResult::SUCCESS(); } (C)2014 Roi Yehoshua Wandering.cpp (8) /*** ROS Subscriptions ***/ void onLaserScanMessage(const sensor_msgs::LaserScan::Ptr laserScanMessage, CallContext* context) { bool obstacleFound = false; int minIndex = ceil((MIN_SCAN_ANGLE_RAD - laserScanMessage->angle_min) / laserScanMessage->angle_increment); int maxIndex = floor((MAX_SCAN_ANGLE_RAD - laserScanMessage->angle_min) / laserScanMessage->angle_increment); for (int i = minIndex; i <= maxIndex; i++) { if (laserScanMessage->ranges[i] < MIN_DIST_TO_OBSTACLE) { obstacleFound = true; } } context->parameters<WorldModel>().obstacleDetected = obstacleFound; } (C)2014 Roi Yehoshua Wandering.cpp (9) int main(int argc, char **argv) { ros::init(argc, argv, "wandering_node"); ros::NodeHandle nh; ros_decision_making_init(argc, argv); // ROS spinner for topic subscriptions ros::AsyncSpinner spinner(1); spinner.start(); // Tasks registration LocalTasks::registrate("driveTask", driveTask); LocalTasks::registrate("turnTask", turnTask); RosEventQueue eventQueue; CallContext context; context.createParameters(new WorldModel()); (C)2014 Roi Yehoshua Wandering.cpp (10) // CallContext must define a team teamwork::SharedMemory db; teamwork::Teammates teammates; teamwork::Team main_team = teamwork::createMainTeam(db, "main", teammates); context.team(TAO_CURRENT_TEAM_NAME, main_team.ptr()); // Subscription for the laser topic and velocity publisher creation ros::Subscriber laserSubscriber = nh.subscribe<void>("base_scan", 1, boost::function<void(const sensor_msgs::LaserScan::Ptr)>(boost::bind(onLaserScanMessage, _1, &context))); _velocityPublisher = nh.advertise<geometry_msgs::Twist>("cmd_vel", 100); eventQueue.async_spin(); ROS_INFO("Starting wandering machine..."); TaoWandering(&context, &eventQueue); eventQueue.close(); ROS_INFO("TAO finished."); return 0; } (C)2014 Roi Yehoshua Wandering Launch File • Copy worlds directory from ~/catkin_ws/src/gazebo_navigation_multi package • Create a launch directory in the tao_wandering package • Add the following wandering.launch file (C)2014 Roi Yehoshua Wandering Launch File <launch> <master auto="start"/> <param name="/use_sim_time" value="true"/> <!-- start gazebo --> <include file="$(find gazebo_ros)/launch/empty_world.launch"> <arg name="world_name" value="$(find tao_wandering)/worlds/willowgarage.world" /> </include> <param name="robot_description" command="$(find xacro)/xacro.py $(find lizi_description)/urdf/lizi.urdf"/> <node name="spawn_urdf" pkg="gazebo_ros" type="spawn_model" args="-x 34 -y 18 -z 0 -Y 1.57 -urdf -param robot_description -model lizi1" output="screen"/> <node name="wandering" pkg="tao_wandering" type="wandering_node" output="screen" /> </launch> (C)2014 Roi Yehoshua Wandering Demo (C)2014 Roi Yehoshua Custom Next Protocol • To define your own next protocol: – Create a class that inherits from decision_making::ProtocolNext – Implement the pure virtual function decide() – Call setDecision at the end of the function with the chosen plan ID (C)2014 Roi Yehoshua Custom Next Protocol • In the following example, we will decompose the Turn plan into TurnLeft and TurnRight subplans • We will create a custom RandomNext protocol that will randomly choose the next plan from its set of candidate plans • We will use this protocol to make the Turn plan randomly choose between TurnLeft and TurnRight as its continuation plan (C)2014 Roi Yehoshua RandomNext Class class NextRandom: public decision_making::ProtocolNext { public: NextRandom(int& res, decision_making::CallContext* call_context, decision_making::EventQueue* events):ProtocolNext(res, call_context, events){} bool decide(){ vector<int> ready_index; for(size_t i = 0; i < options.size(); i++) if (options[i].isReady) ready_index.push_back(i); if (ready_index.size() == 0) return false; int i = _randomizer.uniformInteger(0, ready_index.size() - 1); return setDecision(options[ready_index[i]].id); } }; (C)2014 Roi Yehoshua Turn Plan (1) TAO(Turn) { TAO_PLANS{ Turn, TurnLeft, TurnRight } TAO_START_PLAN(Turn); TAO_BGN { TAO_PLAN(Turn) { TAO_START_CONDITION(WM.obstacleDetected); TAO_ALLOCATE_EMPTY TAO_STOP_CONDITION(true); TAO_NEXT(NextRandom) { TAO_NEXT_PLAN(TurnLeft); TAO_NEXT_PLAN(TurnRight); } } (C)2014 Roi Yehoshua Turn Plan (2) TAO_PLAN(TurnLeft) { TAO_START_CONDITION(true); TAO_ALLOCATE_EMPTY; WM.turnLeft = false; TAO_CALL_TASK(turnTask); TAO_STOP_CONDITION(true); TAO_NEXT_EMPTY; WM.turnFinished = true; } TAO_PLAN(TurnRight) { TAO_START_CONDITION(true); TAO_ALLOCATE_EMPTY; WM.turnLeft = true; TAO_CALL_TASK(turnTask); TAO_STOP_CONDITION(true); TAO_NEXT_EMPTY; WM.turnFinished = true; } } TAO_END } (C)2014 Roi Yehoshua World Model Changes • Add a boolean variable to the world model indicating which turn direction was chosen: struct WorldModel : public CallContextParameters { bool obstacleDetected; bool driveFinished; bool turnFinished; bool turnLeft; string str() const { stringstream s; s << "obstacleDetected = " << obstacleDetected; return s.str(); } }; #define WM TAO_CONTEXT.parameters<WorldModel>() (C)2014 Roi Yehoshua Turn Task • Make the following changes to the turnTask callback: TaskResult turnTask(string name, const CallContext& context, EventQueue& eventQueue) { if (context.parameters<WorldModel>().turnLeft) ROS_INFO("Turning left..."); else ROS_INFO("Turning right..."); geometry_msgs::Twist turnMessage; turnMessage.angular.z = 2 * (context.parameters<WorldModel>().turnLeft ? 1 : -1); int timeToTurnMs = _randomizer.uniformInteger(2000, 4000); int turnLoops = timeToTurnMs / 100; for (int i = 0; i < turnLoops; i++) { _velocityPublisher.publish(turnMessage); boost::this_thread::sleep(boost::posix_time::milliseconds(100.0)); } return TaskResult::SUCCESS(); } (C)2014 Roi Yehoshua Wandering Demo (C)2014 Roi Yehoshua Homework (not for submission) • Create a custom next protocol for the turn plan that will choose between the following 3 plans: – Turn random when there is an obstacle on the front – Turn left when there is an obstacle on the right side – Turn right when there is an obstacle on the left side • Add a support for pausing/resuming the robot by sending an appropriate event (C)2014 Roi Yehoshua