URCap - My First URCap
This article is aimed at URCap Developers.
/MyFirstURCap - Introduction
Through this series of articles, you will be guided through the process of creating your first URCap.
The series will be a section of instructional videos and articles, that will help you understand what steps are necessary, to create a URCap project.
By now, we expect that you have already familiarized yourself with the fundamentals of Universal Robots and URCap.
This, by reading the first trail of "fundamentals" articles, starting with the article "Develop a URCap".
If you have not yet read these, we recommend you go back to URCap Basics and follow the "/Next step" from there.
Throughout the following 6 articles, you will learn:
- What the URCaps Starter Package is, and how to use it.
- How to create a new URCaps Project.
- What the "Service"-class is, and how to develop it.
- How the user interface of your URCap is designed inside the "View"-class.
- What the "Contribution"-class does, and how to develop it.
- Finally, how to build and deploy your URCap, and test it in PolyScope.
It is suggested, that you follow all 6 modules chronologically, but if you are just stuck at a particular spot, you can also choose just to watch this section.
For the sake of demonstration, the MyFirstURCap series will create a new URCap that we call "LightUp".
The purpose of this URCap is that the user can select a particular output on the robot, and a duration.
When the URCap is executed in a program, it will turn on the chosen output for the selected duration, and then turn it back off.
In that sense "lighting up" the output.
By nature, this example is very basic, but whether you are on the path of developing a rather simple, or really complex multi-node URCap, the basic principles are all the same.
Hence, we hope that these articles will aid you in the process of creating your first URCap yourself.
When watching the videos and reading the articles, it may be a benefit to keep the URCaps Starter Package open side-by-side, and co-develop the URCap on yourself at the same time.
MyFirstURCap - Using the Starter Package
In this article, we will demonstrate the basic usage of the URCaps Starter Package.
Video - Using the Starter Package
The Starter Package is a powerful development environment, that enables you to develop and simulate your URCap code.
Inside the Starter Package, you will find the following components:
- Eclipse Java IDE (integrated development environment)
This is where you write your code - URCaps SDK
This is the software development kit, for creating URCaps.
It contains the Java API artefacts as well as samples and documentation. - URSim
Simulators, that closely resemle the PolyScope user interface on a real robot.
This is a great place to test and evaluate your code as you develop.
All these components reside inside a virtual Ubuntu Linux environment.
This means, that you will need a virtualization engine to run the Starter Package.
I.e. VirtualBox or VMWare Workstation Player can be used to execute the virtual machine.
You can download the URCaps Starter Package here:
MyFirstURCap - Creating a new URCap Project
In this video, we will create a new, blank URCap project. The project will be imported into our IDE.
Video - Creating a new URCap Project
In order to create a new URCap project, execute the script newURCap.sh, which is located inside the SDK folder.
When using the Starter Package, you can use the following approach:
- Start a new terminal, pressing CTRL+ALT+T
- Navigate into the desired SDK folder.
If the SDK folder i.e. is located in the home firectory like: /sdk/sdk-1.3.55/
Type the following:
cd sdk/sdk-1.3.55/ - Now, execute the newURCap.sh script
./newURCap.sh - You are prompted to provide a GroupId and ArtifactID for the new URCap.
GroupId: The Group ID represents the base path of the symbolic name of the URCap.
Typically com.yourCompany.yourProject
ArtifactID: The name of this particular URCap
Typically refers to the name of the product the URCap operates.
Enter your desired GroupI and ArtifactID and select OK. - You are prompted to select which version of the API the URCap should be built with.
The newer version, the more features the API have.
URCap will be compatible with the listed PolyScope software version or newer.
Recommendation: Choose the latest API version.
After selecting the desired API version, select OK. - The URCap is now built.
The URCap project folder will be located inside the SDK folder.
The project folder will have the name "<GroupId>.<ArtifactID>", i.e. "com.myCompany.urcap.myGripper". - Optionally, you can choose to copy the project folder to a more suitable location.
- Import the project into your IDE.
In Eclipse, you should select:
File > Import > (Maven) > Existing Maven Projects. - Select Browse from the "Import Maven Projects" dialog.
Locate the project folder, at the location it is stored, and select OK. - The project import wizard will now locate the "pom.xml" file of the project.
Click Finish. - The project is now built and imported.
MyFirstURCap - Creating the Program Node Service
In this video, we will develop the Service-class for our LightUp program node.
Video - Creating the Service
The purpose of the Service-class (SwingProgramNodeService) is to provide some static configuration data about the URCap Program Node.
A more explicit description of the various overridden methods, can be found in the article Principle of Program Node integration.
Since the implementation of the SwingProgramNodeService-interface requires a reference to both the View and Contribution of the node, it is recommended, to create the View and Contribution classes first. This is also demonstrated in the video.
The SwingProgramNodeService-interface requires definition of the nodes ID, as well as the title of the node, as shown in the list of available Program Nodes. Furthermore, the ContributionConfiguration allows modification of the nodes parameter-set, such as if the node can have children (parent node), or not. These settings must be configures with appropriate values.
The Service also references the View and Contribution linked to this node.
Therefore, the Service should return an instance of the View-class when "createView()" is called, and an instance of the Contribution when the "createNode()" is called.
Optionally, these two classes can require the API objects such as the DataModeland API Providers in their constructors.
The LightUpProgramNodeService code from this example, can be found below:
/LightUpProgramNodeService.java
package com.jbminc.lightUp.impl;
import java.util.Locale;
import com.ur.urcap.api.contribution.ViewAPIProvider;
import com.ur.urcap.api.contribution.program.ContributionConfiguration;
import com.ur.urcap.api.contribution.program.CreationContext;
import com.ur.urcap.api.contribution.program.ProgramAPIProvider;
import com.ur.urcap.api.contribution.program.swing.SwingProgramNodeService;
import com.ur.urcap.api.domain.data.DataModel;
public class LightUpProgramNodeService implements SwingProgramNodeService<LightUpProgramNodeContribution, LightUpProgramNodeView>{
@Override
public String getId() {
return "lightUpNode";
}
@Override
public void configureContribution(ContributionConfiguration configuration) {
configuration.setChildrenAllowed(false);
}
@Override
public String getTitle(Locale locale) {
return "Light Up";
}
@Override
public LightUpProgramNodeView createView(ViewAPIProvider apiProvider) {
return new LightUpProgramNodeView(apiProvider);
}
@Override
public LightUpProgramNodeContribution createNode(ProgramAPIProvider apiProvider, LightUpProgramNodeView view,
DataModel model, CreationContext context) {
return new LightUpProgramNodeContribution(apiProvider, view, model);
}
}
When the ProgramNodeService class is complete, this service can be registered in the Activator.
The Activator registers all the services, that the URCap should offer to PolyScope.
When PolyScope starts up, the Activator start()-method is called, and the relevant services should be registered in the bundle context.
In order to register a service, the following command should be invoked in "start()".
bundleContext.registerService(<type of service>, <the service to register>, <properties>);
The type of service, refers to the supported types of Services in the URCapAPI, such as "SwingProgramNodeService", "SwingInstallationNodeService" etc.
While the specific service to register typically will be a new instance of the Service-class you just created, i.e. "new LightUpProgramNodeService()".
In this example, the Activator.java looks like this:
/Activator.java
package com.jbminc.lightUp.impl;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import com.ur.urcap.api.contribution.program.swing.SwingProgramNodeService;
/**
* Hello world activator for the OSGi bundle URCAPS contribution
*
*/
public class Activator implements BundleActivator {
@Override
public void start(BundleContext bundleContext) throws Exception {
System.out.println("LightUp registering!");
bundleContext.registerService(SwingProgramNodeService.class, new LightUpProgramNodeService(), null);
}
@Override
public void stop(BundleContext bundleContext) throws Exception {
}
}
MyFirstURCap - Creating the View
In this video, we will create the user interface of the LightUp URCap.
Video - Creating the View
The View-class represents the user interface of the node in a URCap.
There is only one overridden method in the View-interface; the method "buildUI()".
The method "build()" is only called once. And when called, this provides a JPanel-object as one of the arguments. The JPanel can be considered like a blank piece of paper, that has the size of the program node window. In "buildUI()", the URCap can "draw" all its components onto this paper (JPanel), before the JPanel is shown in the PolyScope user interface.
For each type of URCap node, there is only a single instance of the View, while there can be multiple instances of the program node in a single program.
When this is the case, there is multiple instances of the Contribution, which contains the logic and settings of the individual program node. Therefore, a ContributionProvider-object is also provided as an object in "buildUI()". The ContributionProvider allows the View, to get a hold of the actively selected program node in the program tree, by calling "provider.get()".
The user interface is based on Java Swing, which is a standard Java UI toolkit. Since it is a standard Java toolkit, there are numerous generic online tutorials.
It is generally a good idea, to consider the desired layout, before starting the UI development. This can i.e. be done by drawing the various components, that are required inside the View.
Using a versatile Swing-component such as a Box, allows an easy way to containerize the the different UI elements in the design. In this example, we use a number of private methods, returning a Box with relevant elements. This is shown in i.e. "createDescription()" which creates a Box-object with a configurable JLabel to show description text in the UI.
When the user interacts with an input object, such as a dropdown (JComboBox) or slider (JSlider), this should generate en event in the Contribution, where the configured data is stored. This is handled, by creating EventListeners, that listen for changes on the components. In the EventListener-callbacks, the ContributionProvider is used to get the actively selected Contribution, and set the new value in this Contribution.
In order for the Contribution to be able to configure the settings in the View, the View should implement a number of public methods, that can be called by the active Contribution, to i.e. set the current value in a slider. Failure to do so, will cause "old" values to be present in the View, when there are multiple instances of the program node in a program.
A general principle of binding the View and Contribution together with EventListeners and callbacks can be found in this article.
The View-class code in this example, can be found below:
/LightUpProgramNodeView.java
package com.jbminc.lightUp.impl;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import com.ur.urcap.api.contribution.ContributionProvider;
import com.ur.urcap.api.contribution.ViewAPIProvider;
import com.ur.urcap.api.contribution.program.swing.SwingProgramNodeView;
public class LightUpProgramNodeView implements SwingProgramNodeView<LightUpProgramNodeContribution>{
private final ViewAPIProvider apiProvider;
public LightUpProgramNodeView(ViewAPIProvider apiProvider) {
this.apiProvider = apiProvider;
}
private JComboBox<Integer> ioComboBox = new JComboBox<Integer>();
private JSlider durationSlider = new JSlider();
@Override
public void buildUI(JPanel panel, ContributionProvider<LightUpProgramNodeContribution> provider) {
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
panel.add(createDescription("Select which output to Light Up:"));
panel.add(createSpacer(5));
panel.add(createIOComboBox(ioComboBox, provider));
panel.add(createSpacer(20));
panel.add(createDescription("Select the duration of the Light Up:"));
panel.add(createSpacer(5));
panel.add(createDurationSlider(durationSlider, 0, 10, provider));
}
public void setIOComboBoxItems(Integer[] items) {
ioComboBox.removeAllItems();
ioComboBox.setModel(new DefaultComboBoxModel<Integer>(items));
}
public void setIOComboBoxSelection(Integer item) {
ioComboBox.setSelectedItem(item);
}
public void setDurationSlider(int value) {
durationSlider.setValue(value);
}
private Box createDescription(String desc) {
Box box = Box.createHorizontalBox();
box.setAlignmentX(Component.LEFT_ALIGNMENT);
JLabel label = new JLabel(desc);
box.add(label);
return box;
}
private Box createIOComboBox(final JComboBox<Integer> combo,
final ContributionProvider<LightUpProgramNodeContribution> provider) {
Box box = Box.createHorizontalBox();
box.setAlignmentX(Component.LEFT_ALIGNMENT);
JLabel label = new JLabel(" digital_out ");
combo.setPreferredSize(new Dimension(104, 30));
combo.setMaximumSize(combo.getPreferredSize());
combo.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
if(e.getStateChange() == ItemEvent.SELECTED) {
provider.get().onOutputSelection((Integer) e.getItem());
}
}
});
box.add(label);
box.add(combo);
return box;
}
private Box createDurationSlider(final JSlider slider, int min, int max,
final ContributionProvider<LightUpProgramNodeContribution> provider) {
Box box = Box.createHorizontalBox();
box.setAlignmentX(Component.LEFT_ALIGNMENT);
slider.setMinimum(min);
slider.setMaximum(max);
slider.setOrientation(JSlider.HORIZONTAL);
slider.setPreferredSize(new Dimension(275, 30));
slider.setMaximumSize(slider.getPreferredSize());
final JLabel value = new JLabel(Integer.toString(slider.getValue())+" s");
slider.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
int newValue = slider.getValue();
value.setText(Integer.toString(newValue)+" s");
provider.get().onDurationSelection(newValue);
}
});
box.add(slider);
box.add(value);
return box;
}
private Component createSpacer(int height) {
return Box.createRigidArea(new Dimension(0, height));
}
}
MyFirstURCap - Creating the Contribution
In this video, we will develop the URCap Contribution, for our LightUp program node.
Video - Creating the Contribution
The Contribution stores the logic and behavior of the URCap node.
For each instance of a program node in a program, there exists an instance of the ProgramNodeContribution.
The overall purpose of the Contribution is:
To generate URScript code, that executes the functionality of the program node, based on settings stored in the DataModel.
/DataModel
The configuration of the program node is stored in the DataModel. Each entry has a unique identifier called a key, and the Contribution can set and retrieve values from the DataModel by the key-identifier.
The input to the DataModel generally comes from the user, modifying something in the View. Therefore, the Contribution will typically implement a number of public methods, called by the View, when a change happens.
Inside such a public method, i.e. the "onDurationSelection()" method in the LightUpProgramNodeContribution, the Contribution will store the newly selected duration, by invoking the following DataModel operation:
<dataModel>.set(<key>, <value>);
However since the user in a program has access to Undo- and Redo-functionality, it is critical that the Contribution groups related changes into a single UndoableAction. I.e. if a single user action, caused multiple changes to the DataModel, grouping all these changes as a single UndoableAction allows all these DataModel changes to be undone by a single click on Undo.
/Script Generation
When a user press Play in a program, all the present program nodes and installation nodes are required to deliver URScript. In fact, what is actually executed in real-time by the robot is this URScript. Therefore, the noble purpose of any program node is to generate URScript, that will perform the operation of the node at runtime. When the DataModel is configured, with the program nodes settings, the ProgramNodeContribution should be able to generate the necessary URScript to execute. If the node is not fully configured, i.e. it is not able to execute with the settings present in the DataModel, the "isDefined()" method should return false. If the node could execute with the settings in the DataModel, the node must return true.
The method "generateScript()" is called when the program is played, or saved. As an argument, the method provides a ScriptWriter-object, which needs to be configure with the script generated by the node. This can be done with for instance the following command:
<scriptWriter>.appendLine(<functional line of URScript>);
The ScriptWriter-object has multiple methods, that can be used to configure the necessary URScript. The general syntax of URScript can be found in the URScript Manual.
/User interface control
When the user enters the program node, the method "openView()" is called. "closeView()" is called, when the user navigates away.
These two methods can be used to control the values shown in the user interface (View). Remember: There is only one View, but there may be multiple Contributions.
In this example, we use the openView()-method to i.e. update the value of the duration slider in the View, by calling the following method:
<view>.setDurationSlider(<duration stored in DataModel>);
The LightUpProgramNodeContribution code used in this example, can be found below:
/LightUpProgramNodeContribution.java
package com.jbminc.lightUp.impl;
import com.ur.urcap.api.contribution.ProgramNodeContribution;
import com.ur.urcap.api.contribution.program.ProgramAPIProvider;
import com.ur.urcap.api.domain.data.DataModel;
import com.ur.urcap.api.domain.script.ScriptWriter;
import com.ur.urcap.api.domain.undoredo.UndoRedoManager;
import com.ur.urcap.api.domain.undoredo.UndoableChanges;
public class LightUpProgramNodeContribution implements ProgramNodeContribution{
private final ProgramAPIProvider apiProvider;
private final LightUpProgramNodeView view;
private final DataModel model;
private final UndoRedoManager undoRedoManager;
private static final String OUTPUT_KEY = "output";
private static final String DURATION_KEY = "duration";
private static final Integer DEFAULT_OUTPUT = 0;
private static final int DEFAULT_DURATION = 1;
public LightUpProgramNodeContribution(ProgramAPIProvider apiProvider, LightUpProgramNodeView view,
DataModel model) {
this.apiProvider = apiProvider;
this.view = view;
this.model = model;
this.undoRedoManager = this.apiProvider.getProgramAPI().getUndoRedoManager();
}
public void onOutputSelection(final Integer output) {
undoRedoManager.recordChanges(new UndoableChanges() {
@Override
public void executeChanges() {
model.set(OUTPUT_KEY, output);
}
});
}
public void onDurationSelection(final int duration) {
undoRedoManager.recordChanges(new UndoableChanges() {
@Override
public void executeChanges() {
model.set(DURATION_KEY, duration);
}
});
}
private Integer getOutput() {
return model.get(OUTPUT_KEY, DEFAULT_OUTPUT);
}
private int getDuration() {
return model.get(DURATION_KEY, DEFAULT_DURATION);
}
private Integer[] getOutputItems() {
Integer[] items = new Integer[8];
for(int i = 0; i<8; i++) {
items[i] = i;
}
return items;
}
@Override
public void openView() {
view.setIOComboBoxItems(getOutputItems());
view.setIOComboBoxSelection(getOutput());
view.setDurationSlider(getDuration());
}
@Override
public void closeView() {
}
@Override
public String getTitle() {
return "LightUp: DO"+getOutput()+" t="+getDuration();
}
@Override
public boolean isDefined() {
return true;
}
@Override
public void generateScript(ScriptWriter writer) {
writer.appendLine("set_standard_digital_out("+getOutput()+",True)");
writer.sleep(getDuration());
writer.appendLine("set_standard_digital_out("+getOutput()+",False)");
}
}
MyFirstURCap - Building and deploying the URCap
In this video, we will finalize our URCap.
Then we will build it, and deploy it to PolyScope to test it.
Video - Building and deploying the URCap
How the URCap is build, and later deployed, is configured by the POM.XML file.
To build a URCap project, navigate to the URCap project folder, e.g. "com.yourcompany.urcapname" and execute:
mvn install
Executing just mvn install, will build the URCap. The URCap file (.urcap) can then be found in the project "/target" directory.
If the URCap is configured for deployment to a local instance of a URSim, you can execute:
mvn install -P ursim
In this case, Maven will first build the URCap. Upon a successful build, the URCap is deployed to PolyScope.
Then, PolyScope can be started, and you can test and evaluate your URCap.