5. Client Libraries
The client libraries provide some tools to facilitate the development of a CRTK compatible client over ROS, i.e. create a “proxy” class to communicate with an existing CRTK compatible ROS device. We currently support two client libraries, one for Python and one for Matlab.
The main class in the client library is crtk.utils
. It can be used
to quickly populate an existing class by adding CRTK like
methods. These methods will handle the following for you:
Declare all required ROS publishers and wrap publisher calls in methods to send data to the device
Declare all required ROS subscribers and provide callbacks to receive the data from the device
Convert ROS messages to more convenient data types. The Python client use numpy arrays for joint values and PyKDL types for cartesian data. The Matlab client uses vectors and matrices.
Some events to manage asynchronous communication between the device and the “proxy” class (e.g. end of
move
command, change of state…)
The class crtk.utils
is designed to add CRTK features “a la
carte”, i.e. it doesn’t assume that all CRTK features are
available. This allows to:
Match only the features that are available on the CRTK devices one wants to use (server side)
Reduce the number of features to those strictly needed for the application (client side). Reducing the number of ROS topics used helps in terms of performance.
In version 1.2, crtk
also includes a ROS Abstraction Layer. The
ral
goal is to hide which version of ROS is used so the end-user
script can run with either ROS 1 or ROS 2. The ral
doesn’t cover
all the ROS features (either in Python or Matlab) so not all scripts
can be totally portable.
Note
As of December 2023, the Python client libraries have been used extensively and are well tested. The Matlab version is not as mature. More specifically the RAL implementation is buggy on ROS 2 and missing in the ROS 1 implementation.
6. Client Libraries: Python
6.1. Installation
Note
If you’ve installed a package relying on CRTK using a tool such as
vcs
, it is quite possible all the CRTK dependencies have
already been pulled from GitHub. Before pulling another copy,
check the content of your ROS workspace src
directory.
6.1.1. ROS 1
To build the ROS 1 CRTK Client library, we recommend to use the catkin
build tools, i.e. catkin build
. You will need to clone this repository
as well as the repository with CRTK specific ROS messages:
cd ~/catkin_ws/src # or wherever your catkin workspace is
git clone https://github.com/collaborative-robotics/crtk_msgs crtk/crtk_msgs
git clone https://github.com/collaborative-robotics/crtk_python_client crtk/crtk_python_client
catkin build
Once these packages are built, you should source your setup.bash
:
source ~/catkin_ws/devel/setup.bash
. At that point, you should be able
to import the crtk python package in Python using import crtk
.
6.1.2. ROS 2
You will first need to clone this repository as well as the repository with CRTK specific ROS messages:
cd ~/ros2_ws/src # or wherever your ROS 2 workspace is
git clone https://github.com/collaborative-robotics/ros2_crtk_msgs crtk/crtk_msgs
git clone https://github.com/collaborative-robotics/ros2_crtk_python_client crtk/crtk_python_client
cd ~/ros2_ws;
colcon build
Once these packages are built, you should source your setup.bash
:
source ~/ros_ws/install/setup.bash
. At that point, you should be able
to import the crtk python package in Python using import crtk
.
6.2. Setting up a client
6.2.1. Overview
You can find some examples in the scripts directory. Overall, the
approach is always the same, i.e. create a class and populate it with
crtk.utils
. You can then use an instance of this class. For
example:
class crtk_move_cp_example:
# constructor
def __init__(self, ral):
self.ral = ral # keep a copy for future use
# populate this class with all the ROS topics we need
self.crtk_utils = crtk.utils(self, ral)
self.crtk_utils.add_measured_cp()
self.crtk_utils.add_move_cp()
What is happening behind the scene:
ral
is the ROS Abstraction Layer used by the device. It contains information about the ROS node as well as the namespace. For example, if the namespace isleft
, we assume the device will have its CRTK ROS topics under/left
Add an instance of
crtk.utils
to your class. The first parameter indicates which Python object should be “populated”, i.e. which object will have the CRTK methods added to its dictionary. The second parameter is the ROS Abstraction Layer. Theral
will keep track of all the publishers and subscribers used.
add_measured_cp()
:
Creates a subscriber for the topic, e.g.
/left/measured_cp
Registers a built-in callback for the topic. The callback will store the latest
measured_cp
ROS message incrtk_utils
Provides a method to read the latest
measured_cp
message as a PyKDL frameAdds the method
measured_cp()
to the user class (crtk_move_cp_example
)
add_move_cp()
:
Creates a publisher for the topic, e.g.
/left/move_cp
Provides a method to send a PyKDL frame (goal), internally converts to a ROS message
Adds the method
move_cp()
to the user class (crtk_move_cp_example)
Once the class is defined, the user can use it:
ral = crtk.ral('node_name', '/left') # the node name is mandatory, default namespace is empty
example = crtk_move_cp_example(ral)
# make sure all the subscribers (resp. publishers) are connected to one or more publisher (resp. subscriber)
ral.check_connections()
# make sure ROS spin is happening (no-op on ROS 1, needed on ROS 2)
ral.spin()
position = example.measured_cp()
position.p[2] += 0.05 # move by 5 cm
example.move_cp(position)
# stop
ral.shutdown()
6.2.2. Operating state
crtk.utils.add_operating_state
adds:
State status
operating_state()
and helper queries:is_enabled()
,is_homed()
,is_busy()
State command
operating_state_command()
and helper commands:enable()
,disable()
,home()
,unhome()
Timer/event utilities:
For subscribers:
wait_for_valid_data()
For publishers (used by move commands):
wait_for_busy()
For state changes (used by
enable()
,home()
…):wait_for_operating_state()
6.2.3. Robot motion
crtk.utils
supports the following CRTK features:
Subscribers:
add_setpoint_js
,add_setpoint_cp
add_measured_js
,add_measured_cp
,add_measured_cv
,add_measured_cf
…
Publishers:
add_servo_jp
,add_servo_jf
,add_servo_cp
,add_servo_cf
…add_move_jp
,add_move_cp
All methods relying on subscribers to get data have the following two optional parameters: age
and wait
:
setpoint_cp(age = None, wait = None)
The parameter age specifies how old the data can be to be considered
valid and wait specifies how long to wait for the next message if the
data currently cached is too old. By default, both are based on the
expected interval provided when creating an instance of
crtk.utils
. The expected interval should match the publishing rate
from the CRTK device. Setting the age to zero means that any cached
data should be used and the method shouldn’t wait for new messages.
All move commands (move_jp
and move_cp
) return a ROS time
object. This is the time just before sending (i.e., publishing) the
move command to the device. This timestamp can be used to wait for
motion completion using:
# wait until robot is not busy, i.e. move has ended
h = example.move_cp(goal) # record time move was sent
h.wait()
# compact syntax
example.move_cp(goal).wait()
# other example, wait until move has started
example.move_cp(goal).wait(is_busy = True)
The method wait_for_busy()
used by handle.wait()
depends on the CRTK
device operating state and can be added to the example class using
crtk.utils.add_operating_state
. See previous section.
6.2.4. RAL
The ROS Abstraction Layer (RAL) allows programmers to write Python
scripts that can be easily ported between ROS 1 (rospy
) and ROS 2 (rclpy
).
6.2.4.1. Spinning
One of the main differences between the ROS 1 and ROS 2 Python APIs is
that rospy
handles the spinning thread behind the scene. So one
can create a subscriber with a callback but it’s impossible to know
when the spin
happens, i.e. when the incoming ROS messages are
being processed. In rclpy
, one has to create their own thread and
make sure this thread calls spin
. When the application ends, the
programmer must also ensure that the “spinning” thread is stop
cleanly. RAL provides a few methods to manage this thread. These
methods are not needed for ROS 1 scripts but if the goal is to use
your script with ROS 2 later on, it’s a good habit to use them:
spin()
: starts an executor and makes sure thespin
method is called
spin_and_execute(f, *args)
: callsral.spin
and then the functionf
with the arguments*args
shutdown()
: calls ROS shutdown and stops the “spinning” thread
on_shutdown(cb)
: register a callback for shutdown
is_shutdown()
: check if the RAL is shut down
6.2.4.2. Time wrappers
now()
:
get_timestamp(t)
: get best timestamp for objectt
, representation is ROS version dependent
to_sec(t)
: get best timestamp for objectt
, result is a floating point
create_duration(d_seconds)
: create a duration object, the type is ROS version dependent but the APIs are similar
create_rate(r_hz)
: create a rate object, the type is ROS version dependent but the APIs are similar
6.2.4.3. Create publishers, subscribers and service clients
publisher(self, topic, msg_type, queue_size = 10, latch = False, *args, **kwargs)
subscriber(self, topic, msg_type, callback, queue_size=10, latch = False, *args, **kwargs)
service_client(self, name, srv_type)
6.2.4.4. Checking connections
Besides abstracting ROS 1 rospy
and ROS 2 rclpy
, the RAL is
also used to keep track of all the publishers and subscribers created
for the Python client. This allows to check if there are existing
nodes subscribing to the client publishers and nodes publishing to the
client subscribers. This is useful for a few reasons:
Detecting mismatches between ROS namespaces. One can easily have a typo or simply use the wrong namespace. If this is the case,
ral.check_connection()
will timeout and throw an exceptionDetecting a missing feature on the server side. The client might expect a CRTK feature from the server (device) but that feature doesn’t exist. In this case,
ral.check_connection()
will throw an exception. The list of missing CRTK features will be provided in the exception message.Avoiding race conditions when starting a node. With ROS, one can create a publisher and immediately publish. The issue is that even if there is already a subscriber for the same topic, it is possible to publish before the subscriber is connected. The connection process is fast but it is still possible to use
publish
before the subscriber is connected. To avoid this,ral.check_connection()
performs a busy wait, checking all the node’s publishers and subscribers and returns when they are all connected. The default time-out is 5 seconds.
6.3. Using a client
For this section we will use the dVRK since it comes with a set of
ready-to-use classes based on the CRTK Python Client. One can use the
classes dvrk.arm
, dvrk.psm
, dvrk.mtm
… that rely on
crtk.utils
to provide as many CRTK features as possible.
Note
The client’s developer has to make some choices regarding which features to provide. For an interactive session (say iPython) or quick prototyping, it’s useful to have as many features as possible. The issue is that each subscriber has a cost. For one thing, the publisher will have to publish. Then the Python client will receive the message and the callback will be called. This extra cost will incur whether the feature is used of not. Therefore, if performances are an issue, it is recommended to create a custom client with the just required features and nothing else.
The dVRK arm class implementation can be found in the “dvrk_python” package.
Example of use:
import crtk, dvrk
ral = crtk.ral('PSM1')
p = dvrk.arm(ral)
ral.check_connections()
ral.spin()
p.enable()
p.home()
# get measured joint state
[position, velocity, effort, time] = p.measured_js()
# get only position
position = p.measured_jp()
# get position and time
[position, time] = p.measured_jp(extra = True)
# move in joint space
import numpy
p.move_jp(numpy.array([0.0, 0.0, 0.10, 0.0, 0.0, 0.0]))
# move in cartesian space
import PyKDL
# start position
goal = p.setpoint_cp()
# move 5cm in z direction
goal.p[2] += 0.05
p.move_cp(goal).wait()
import math
# start position
goal = p.setpoint_cp()
# rotate tool tip frame by 25 degrees
goal.M.DoRotX(math.pi * 0.25)
p.move_cp(goal).wait()
ral.shutdown()
7. Client Libraries: Matlab
7.1. Installation
To note, rosgenmsh
(ROS 1) and ros2genmsg
(ROS 2) have one
rather annoying limitation, they expect a directory that contains
directories of ROS packages. So if you clone crtk_msgs
in
my_ws/src
you will need to use rosgenmsg
(or ros2genmsg
)
on all packages in my_ws/src
. To avoid this, you need to create a
sub-directory in my_ws/src
and then clone crtk_msgs
in said
sub-directory (for example ~/my_ws/src/crtk/crtk_msgs
).
Matlab uses pretty similar names for the ROS 1 and ROS 2 commands so
the following instructions are valid for both ROS 1 and ROS 2.
Instructions below are for ROS 2. For ROS 1, replace ros2_ws
by
catkin_ws
, ros2genmsg
by rosgenmsg
and colcon build
by
catkin_build
.
Note
If you’ve installed a package relying on CRTK using a tool such as
vcs
, it is quite possible all the CRTK dependencies have
already been pulled from GitHub. Before pulling another copy,
check the content of your ROS workspace src
directory.
If you’re cloning crtk_msgs
by hand, you can use the following instructions.
7.1.1. ROS 1
cd ~/catkin_ws/src
# clone in sub-directory crtk and rename to crtk_msgs
git clone https://github.com/collaborative-robotics/crtk_msgs crtk/crtk_msgs
git clone https://github.com/collaborative-robotics/crtk_matlab_client crtk/crtk_matlab_client
cd ~/catkin_ws
catkin build
7.1.2. ROS 2
cd ~/ros2_ws/src
# clone in sub-directory crtk and rename to crtk_msgs
git clone https://github.com/collaborative-robotics/ros2_crtk_msgs crtk/crtk_msgs
git clone https://github.com/collaborative-robotics/ros2_crtk_matlab_client crtk/crtk_matlab_client
cd ~/ros2_ws
colcon build
7.2. Custom message generation
At that point, you can finally generate the code in Matlab:
ROS 1:
rosgenmsg('~/catkin_ws/src/crtk')
ROS 2:
ros2genmsg('~/ros2_ws/src/crtk')
You could follow the instructions displayed at the end of the message
generation to use savepath
but this would apply to all users on
the workstation. So you should probably skip that step and read the
following section so the paths would be defined per user.
Instead of setting the path for all users, you can use your startup
script. To edit the user’s startup.m
in Matlab, use:
edit(fullfile(userpath,'startup.m'))
If the file doesn’t exist yet, click yes to create it. In your
startup.m
, you can add the addpath
commands that you want
executed everytime your start Matlab:
Note
In the following, the path to find the generated code varies based
on the ROS and Matlab versions, make sure you use the path provided
by Matlab at the end of the rosgenmsg
call.
ROS 1:
% to locate crtk_msgs
% some Matlab versions use the following
addpath('~/catkin_ws_ws/src/crtk/matlab_msg_gen_XXXXXXXXXXX')
% to locate crtk client
addpath('~/catkin_ws/src/crtk/crtk_matlab_client')
% to locate dvrk code - only for dVRK users
addpath('~/catkin_ws/src/dvrk-ros/dvrk_matlab')
ROS 2:
% to locate crtk_msgs
% some Matlab versions use the following
addpath('~/ros2_ws/src/crtk/matlab_msg_gen')
% to locate crtk client
addpath('~/ros2_ws/src/crtk/crtk_matlab_client')
% to locate dvrk code - only for dVRK users
addpath('~/ros2_ws/src/dvrk/dvrk_matlab')
Then quit Matlab, restart it and test using:
clear classes
rehash toolboxcache
which startup
% create a message (ROS 1)
m = rosmessage('crtk_msgs/OperatingState')
% create a message (ROS 2)
m = ros2message('crtk_msgs/OperatingState')
7.3. Setting up a client
The first step is to create a Matlab class with dynamic properties. For example, let’s assume we want to create a simple force sensor client:
classdef force_sensor < dynamicprops
The class should own an instance of crtk_utils
:
properties (Access = protected)
crtk_utils;
end
Then in the constructor, create an instance of crtk_utils
and add
the CRTK features you need. For example, if the device supports
measured_cf
, use the method add_measured_cf()
.
methods
function self = force_sensor(ros_namespace)
self.crtk_utils = crtk.utils(self, ros_namespace);
self.crtk_utils.add_measured_cf();
end
end
The method add_measured_cf
will create the necessary ROS subscriber
and add a function handle (measured_cf
) to the force sensor class.
Once this is done, you can create an instance of the force sensor and
call the method measured_cf
:
>> fs = force_sensor('optoforce/');
>> cf = fs.measured_cf()
cf =
-0.0025 -0.0125 0.0775 0 0 0
If there are no messages on the CRTK topic subscribed to, you will get a warning similar to:
>> cf = fs.measured_cf()
Warning: measured_cf has not received messages yet (topic /optoforce/measured_cf)
This can be used to make sure you’re using the right ROS topic name and namespace.
7.4. Using a client
This example is based on the dVRK Matlab package.
ral = crtk.ral('test_arm_move');
r = dvrk.arm(arm_name, ral);
disp('---- Enabling (waiting up to 30s)');
if ~r.enable(30.0)
error('Unable to enable arm');
end
disp('---- Homing (waiting up to 30s)');
if ~r.home(30.0)
error('Unable to home arm');
end
% general settings
rate = 200; % aiming for 200 Hz
ros_rate = ral.rate(rate);
% move_jp
disp('---- Joint move');
% move to 0 position
joints_home = r.setpoint_js();
joints_home(:) = 0.0;
if (strcmp(arm_name, 'ECM') || strncmp(arm_name, 'PSM', 3))
joints_home(3) = 0.12;
end
r.move_jp(joints_home).wait();
% wiggle first two joints, matlab index starts at 1
amplitude = deg2rad(10.0);
% first move
start = r.setpoint_js();
goal = start;
goal(1:2) = amplitude;
r.move_jp(goal).wait();
% second move
goal = start;
goal(1:2) = -amplitude;
r.move_jp(goal).wait();
disp('---- Joint servo');
% move to 0 position
r.move_jp(joints_home).wait();
% wiggle first two joints, matlab index starts at 1
amplitude = deg2rad(10.0);
duration = 10.0; % seconds
samples = duration * rate;
% create a new goal starting with current position
start = r.setpoint_js();
goal = start;
reset(ros_rate);
for i = 0:samples
goal(1) = start(1) + amplitude * (1.0 - cos(i * deg2rad(360.0) / samples));
goal(2) = start(2) + amplitude * (1.0 - cos(i * deg2rad(360.0) / samples));
r.servo_jp(goal);
waitfor(ros_rate);
end