Tutorial¶
10 Apr 2021
PLEXIL is a language used to represent plans of automation. To help new users learn the language without having a system to automate, we have created the Robosim simulator. This tutorial will guide you through learning PLEXIL using this simulator.
Throughout this tutorial links to the PLEXIL Documentation will be provided so you can read more about the topics we explore.
1. Starting the simulation¶
Once you have PLEXIL installed you can navigate to $PLEXIL_HOME/examples/robosim
and run:
$ ./robosim start
This command runs an IPC server and the Robosim simulation in the background of your terminal. You should see the Robosim application launch, this is the environment we’ll be working with for this tutorial.
To restart the simulation or stop both the simulation and the IPC server run
$./robosim restart
or
$./robosim stop
Troubleshooting¶
If the application fails to launch you may have to run
$ make
in one of these two locations:
$PLEXIL_HOME/
or $PLEXIL_HOME/examples/robosim/2DRobotSimulator
2. Your first node and command¶
Let’s start by making a PLEXIL file. inside
$PLEXIL_HOME/examples/robosim/plans
, create a new file called
Drive.ple
.
A PLEXIL program is made up of nodes that consist of a pair of curly
braces: { }
. Each PLEXIL plan should have one root node, let’s
create it now.
Drive:
{
}
We named this node Drive
. Now lets make our program do something!
PLEXIL programs interact with their environment using commands provided
by the system you are automating. Let’s declare and use the utility
command pprint()
.
Command pprint(...);
Drive:
{
pprint("Hello World"); // Prints 'Hello World' to the unix terminal
}
3. Compile and run a PLEXIL plan¶
To run a PLEXIL program you must first compile it:
$ plexilc Drive.ple
This will generate a compiled xml file called Drive.plx
. We can run
the program with:
$ plexilexec -p Drive.plx
You should see 'Hello World'
printed to the terminal.
4. Moving a robot¶
Robots first step:¶
Now let’s start working with the robosim. Start by declaring and using the move command
Integer Command Move(String name, Integer direction);
Drive:
{
String robotName = "RobotYellow";
Move(robotName, 1);
}
Let’s make sure we understand whats going on in each line:
Integer Command Move(String name, Integer direction);
Here we declare the command that tells the simulator to move the robot. You can read more about commands here, and see the commands supported by the robosim here.
String robotName = "RobotYellow";
Next we declare and initialize a variable of type String to hold the name of the robot we want to control. PLEXIL supports several data types that can be found here.
Move(robotName, 1);
Finally we call the move command to move the Yellow Robot right (direction 1) once. (We aren’t using the return value of Move yet)
Compile the program with plexilc
. To run a program that uses the
robosim commands we have to provide the robosim interface like this:
$ plexilexec -p Drive.plx -c ../interface-config.xml
Continuous motion:¶
So how do we move the robot more than once? There are two options; one is to use a loop, the second is to use a RepeatCondition. Lets look at a while loop.
Integer Command Move(String name, Integer direction);
Drive:
{
String robotName = "RobotYellow";
Boolean run = true;
while (run)
{
Integer moveResponse;
Move:
{
EndCondition isKnown(moveResponse); // Wait for the response after moving the robot
moveResponse = Move(robotName, 1); // Direction 1 means move right
}
if (moveResponse != 1) // A response of 1 means the move succeeded
run = false; // If the move failed stop the run
}
}
We can see that the robot will continue to move until the move fails. This can happen if the robot hits an obstacle, or runs out of battery. Feel free to try both methods and learn about RepeatCondition and loop.
5. Adding Intelligence¶
Now let’s teach the robot to path to an energy source.
These are the commands we are going to need so we must declare them at the top of out file first.
Integer Command Move(String name, Integer direction);
Real[5] Command QueryEnergySensor(String name);
Integer[4] Command QueryVisibilitySensor(String name);
Next we have to call these commands to figure out which direction an energy source is and in which directions we can move.
Drive:
{
String robotName = "RobotYellow";
Boolean run = true;
while (run)
Concurrence // this tells our nodes to execute concurrently allowing us to read both sensors at the same time
{
Real energyVals[5]; // Declare arrays to hold our query data
Integer visibilityVals[4];
Integer direction = 0; // The direction we will go
ReadEnergySensor:
{
// end the node when the query has returned values
EndCondition isKnown(energyVals[0]);
energyVals = QueryEnergySensor(robotName);
}
ReadVisibilitySensor:
{
// end the node when the query has returned values
EndCondition isKnown(visibilityVals[0]);
visibilityVals = QueryVisibilitySensor(robotName);
}
}
}
Now that we have the data from our sensors we must figure out which direction to move in.
FindDirection:
{
Real maxVal = 0; // initialize a variable to track the best direction
// Make sure we only start this node after we have our sensor data
StartCondition isKnown(energyVals[0]) && isKnown(visibilityVals[0]);
// Loop through every direction we could go
for(Integer i = 0; i < 4; i + 1>)
{
// If the direction is unoccupied and has the highest energy value we save it into the variable direction
if(visibilityVals[i] == 1 && energyVals[i] > maxVal)
{
maxVal = energyVals[i];
direction = i;
}
endif
}
// If our next move will take us to an energy source we can stop the run
if(maxVal== 1)
run = false;
}
The only thing left to do is make the move:
Move:
{
// only start once we know what direction to go
StartCondition FindDirection.outcome == SUCCESS;
Move(RobotName, direction)
}
Now lets put it all together:
Integer Command Move(String name, Integer direction);
Real[5] Command QueryEnergySensor(String name);
Integer[4] Command QueryVisibilitySensor(String name);
Drive:
{
String robotName = "RobotYellow";
Boolean run = true;
while (run)
Concurrence // this tells our nodes to execute concurrently allowing us to read both sensors at the same time
{
Real energyVals[5]; // Declare arrays to hold our query data
Integer visibilityVals[4];
Integer direction = 0; // The direction we will go
ReadEnergySensor:
{
// end the node when the query has returned values
EndCondition isKnown(energyVals[0]);
energyVals = QueryEnergySensor(robotName);
}
ReadVisibilitySensor:
{
// end the node when the query has returned values
EndCondition isKnown(visibilityVals[0]);
visibilityVals = QueryVisibilitySensor(robotName);
}
FindDirection:
{
Real maxVal = 0; // initialize a variable to track the best direction
// Make sure we only start this node after we have our sensor data
StartCondition isKnown(energyVals[0]) && isKnown(visibilityVals[0]);
// Loop through every direction we could go
for(Integer i = 0; i < 4; i + 1)
{
// If the direction is unoccupied and has the highest energy value we save it into the variable direction
if(visibilityVals[i] == 1 && energyVals[i] > maxVal)
{
maxVal = energyVals[i];
direction = i;
}
endif
}
// If our next move will take us to an energy source we can stop the run
if(maxVal == 1)
run = false;
}
Move:
{
// only start once we know what direction to go
StartCondition FindDirection.outcome == SUCCESS;
Move(robotName, direction);
}
}
}
Now you can watch your work in node! Compile and run the code on the robosim and watch the robot path to the nearest energy source. If you want you can show the detection range of the energy sources in the sim by pressing ‘e’ the goal by pressing ‘g’ and the flags by pressing ‘f’.
6. Library calls¶
Abstraction is a key idea when programming that allows the creation of more organized less error prone code. In PLEXIL abstraction is achieved through library calls. In our example we would like our robot to move to several different objects, the energy sources, the flags, and the goals. To avoid copied code we will write a library node that determines the best direction to go for any of these objects.
Because each PLEXIL file may only have one top level node we will write
our library node in a new file called BestDirection.ple
.
BestDirection:
{
In Real directionVals[4]; // The value of moving in each direction (Read Only)
In Integer visibilityVals[4]; // The visibility value of each direction (Read Only)
InOut Integer maxDirection; // An out-parameter used to return the best direction to the calling node.
Real maxVal = 0; // Track the max val just like before
StartCondition isKnown(directionVals[0]);
for(Integer i = 0; i < 4; i + 1)
{
if(visibilityVals[i] == 1 && directionVals[i] > maxVal)
{
maxDirection = i;
maxVal = directionVals[i];
}
endif
}
}
Our library node will accept an array of reals representing the goal,
energy, or flag values in each of the 4 directions and return through
the out parameter maxDirection
the direction with the largest value.
Now lets use this library node in a plan that will path our robot to the goal, picking up energy sources along the way if need be.
First we declare our robosim Commands and our library node:
Real[5] Command QueryGoalSensor(String name);
Real[5] Command QueryEnergySensor(String name);
Integer[4] Command QueryVisibilitySensor(String name);
Real[4] Command QueryRobotState(String name);
Integer Command Move(String name, Integer direction);
LibraryAction BestDirection (In Real directionVals[4],
In Integer visibilityVals[4],
InOut Integer maxDirection);
Next lets create our main loop and read the sensors:
Drive:
{
String robotName = "RobotYellow";
Boolean atGoal = false;
Boolean noBattery = false;
// Fail if we ever run out of battery
InvariantCondition !noBattery;
// Succeed if we ever reach the goal
ExitCondition atGoal;
while(!atGoal && !noBattery) {
Real robotState[4];
Real energyVals[5];
Real goalVals[5];
Integer visibilityVals[4];
Integer direction;
ReadRobotState:
{
EndCondition isKnown(robotState[0]);
robotState = QueryRobotState(robotName);
}
ReadEnergySensor:
{
EndCondition isKnown(energyVals[0]);
energyVals = QueryEnergySensor(robotName);
}
ReadGoalSensor:
{
EndCondition isKnown(goalVals[0]);
goalVals = QueryGoalSensor(robotName);
}
ReadVisibilitySensor:
{
EndCondition isKnown(visibilityVals[0]);
visibilityVals = QueryVisibilitySensor(robotName);
}
}
}
Now we use the sensor data to determine which direction we should go:
DetermineDirection:
{
// If we are below 50% energy then move to energy
if(robotState[2] < .5)
LibraryCall BestDirection(directionVals=energyVals,
visibilityVals=visibilityVals,
maxDirection=direction);
endif
// If we have energy or we are not in range to detect any move towards the goal.
if(!isKnown(direction))
LibraryCall BestDirection(directionVals=goalVals,
visibilityVals=visibilityVals,
maxDirection=direction);
endif
// If we still have no direction move down
if(!isKnown(direction))
direction = 2;
// check if we are at the goal or if we are out of battery.
if(robotState[2] == 0)
noBattery = true;
endif
if(goalVals[4] == 1)
atGoal = true;
endif
}
Finally we just need to move in the direction we found:
Move:
{
StartCondition isKnown(direction);
// if we haven't reached the goal continue moving
if(goalVals[4] != 1)
Move(robotName, direction);
}
Lets put it all together:
Real[5] Command QueryGoalSensor(String name);
Real[5] Command QueryEnergySensor(String name);
Integer[4] Command QueryVisibilitySensor(String name);
Real[4] Command QueryRobotState(String name);
Integer Command Move(String name, Integer direction);
LibraryAction BestDirection (In Real directionVals[4],
In Integer visibilityVals[4],
InOut Integer maxDirection);
Drive:
{
String robotName = "RobotYellow";
Boolean atGoal = false;
Boolean noBattery = false;
// Fail if we ever run out of battery
InvariantCondition !noBattery;
// Succeed if we ever reach the goal
ExitCondition atGoal;
while(!atGoal && !noBattery) {
Real robotState[4];
Real energyVals[5];
Real goalVals[5];
Integer visibilityVals[4];
Integer direction;
ReadRobotState:
{
EndCondition isKnown(robotState[0]);
robotState = QueryRobotState(robotName);
}
ReadEnergySensor:
{
EndCondition isKnown(energyVals[0]);
energyVals = QueryEnergySensor(robotName);
}
ReadGoalSensor:
{
EndCondition isKnown(goalVals[0]);
goalVals = QueryGoalSensor(robotName);
}
ReadVisibilitySensor:
{
EndCondition isKnown(visibilityVals[0]);
visibilityVals = QueryVisibilitySensor(robotName);
}
DetermineDirection:
{
// If we are below 50% energy then move to energy
if(robotState[2] < .5)
LibraryCall BestDirection(directionVals=energyVals,
visibilityVals=visibilityVals,
maxDirection=direction);
endif
// If we have energy or we are not in range to detect any move towards the goal.
if(!isKnown(direction))
LibraryCall BestDirection(directionVals=goalVals,
visibilityVals=visibilityVals,
maxDirection=direction);
endif
// If we still have no direction move down
if(!isKnown(direction))
direction = 2;
// check if we are at the goal or if we are out of battery.
if(robotState[2] == 0)
noBattery = true;
endif
if(goalVals[4] == 1)
atGoal = true;
endif
}
Move:
{
StartCondition isKnown(direction);
// if we haven't reached the goal continue moving
if(goalVals[4] != 1)
Move(robotName, direction);
}
}
}
Try running your code and watch your robot go! Don’t forget to compile your library node.
7. Further exploration¶
Now it’s time to explore the language on your own…
Can you make the robot pick up the flag and take it to the goal?
Can you control two robots at once?
Explore some more advanced feature of the language like lookups or the other types of nodes not covered here.
Originally Written by Bryce Campbell (2020)