Apps / APIs

To control and use HEBI hardware we provide Applications and APIs that run on a wide variety of operating systems. Apps include GUIs that provide a user-friendly interface for things like viewing feedback, configuring modules, or getting feedback from other devices. APIs allow you to write programs in different languages that interface with HEBI hardware. All of these apps and APIs can be found in the Downloads section.

App Description OS Support

A GUI for doing basic tasks on modules like visualizing feedback in real-time, manually sending commands, setting name/family, configuring controller, tuning gains, and updating firmware.

Windows (Win64)
Linux (x86-64)
macOS (64-bit)

A mobile app that provides basic user input on a touch screen and provides easy access to a number of internal sensors from the mobile device. The app provides on-screen buttons and sliders for sending digital and analog inputs, mirroring the physical capabilities of the HEBI I/O Board, that can be read in the various APIs, including a layout that acts as a joystick.

iOS
Android (in development)


API Description OS Support

The Matlab API is a released API that runs in the standard Matlab working environment with no additional add-ons or toolboxes.

Windows (Win64)
Linux (x86-64)
macOS (64-bit)

The Python API is in a beta testing stage. It is available at https://pypi.python.org/pypi/hebi-py.

Windows (Win32 / Win64)
Linux (i686 / x86-64 / armhf / aarch64)
macOS (64-bit)

C++

The C++ API is a released API that is provided as source code wrapping a C library to guarantee maximum portability across compilers, platforms, and operating systems.

Windows (Win32 / Win64)
Linux (i686 / x86-64 / armhf / aarch64)
macOS (64-bit)

ROS

The ROS API is in a beta testing stage. It is documented and available for download at http://wiki.ros.org/hebiros.

Linux (i686 / x86-64 / armhf / aarch64)

C#

The C# API is in a beta testing stage, and will be released on the NuGet package management system.

Windows (Win32 / Win64)

C

A low-level API that is generally not intended for direct use.

Windows (Win32 / Win64)
Linux (i686 / x86-64 / armhf / aarch64)
macOS (64-bit)


File Format Description Supports

A format for saving and loading all the control parameters for the onboard controllers for individual modules, as well as groups of modules.

Scope
All APIs

A format that allows saving and loading of the kinematics and dynamics information about a robot configuration.

Scope
All APIs

Scope

scope icon 512

Scope is a cross-platform graphical user interface (GUI) tool that provides configuration, monitoring, and testing capabilities for HEBI actuators and modules. We recommend using it at all stages of your development. Before writing a new program Scope is useful in order to make sure that all devices are connected properly and that the system as a whole is working as expected. While developing and debugging a new application Scope is extremely useful for viewing the feedback of modules.

Getting Started


initial screen


Scope will start with a screen that looks similar to the screenshot above. The numbered items in the screenhot are, repectively:

  1. The Device Panel that shows all devices visible on the network interface.

  2. The Main Panel shows information of the currently active tab.

  3. Tabs for the various panels:

    • Information shows general device information such as hardware revisions and Ethernet settings.

    • Monitoring provides plots of device feedback as well as manual inputs for position, velocity, and effort.

    • Gains sets control strategies, gains, and safety limits.

    • Device provides access to hardware features such as zeroing sensors.

  4. A toggle for the Device Panel between showing settable human readable names, or the corresponding hardware types and serial numbers.

  5. A checkbox and slider for controlling the rate of background lookup requests.

  6. A button to check for firmware updates for any of the devices that are currently in the device tab.

  7. A drop-down for selecting the active network interface. For example, if your computer is on a separate wired and wireless network, you can select which network to show in the in the Device Panel. You can show all connected networks by selecting 'ALL'. If a network connection is connected or disconnected you can update the drop-down items by clickign the 'Refresh' button.

Device Panel

The device panel shows all devices visible on the network. The display can be toggled between showing settable human readable names, or the corresponding hardware types and serial numbers.

The colors indicate the current status:

  • Blue text: device is reachable and in Bootloader mode

  • Black text: device is reachable and in Application mode

  • Grey text: device has not been reachable for at least 5 seconds

A right-click on one or multiple selected devices brings up a context menu with various useful actions such as sorting the device list, changing the name used for identification, rebooting, or setting the LED color to help determine a module’s location on a robot.


device set led


Note that some device parameters that get changed frequently (e.g. gains) are stored in memory and will reset when the actuator restarts. The persist button in the context menu forces the currently active parameters to be stored across reboots.

If one or more devices are not shown in the device panel, try the following:

  • Update the network interfaces by clicking the 'Refresh' button in the top bar.

  • Confirm that the background lookup is enabled in the top bar.

  • Confirm that each device has successfully booted up by looking at the LED Status Codes

  • Confirm that the computer that Scope is running on is on the same network

Information Tab

The Information tab shows general device information such as hardware revisions and Ethernet settings.

tab information labels
  1. The Hardware and Electronics sections show the details of the mechanical electrical components of the module. These items are permanent and will never change.

  2. The Firmware section shows the type of firmware (which will typically match the 'Electronics' type), as well as the revision number of firmware the module is running. The revision number will change as you update to the latest firmware.

  3. The Ethernet section that shows the MAC Address of the module, which is permanent, as well as network parameters that will depend on the details of your network configuration.

Monitoring Tab

The Monitoring Tab provides a way to plot device feedback and provide basic inputs for position, velocity, and effort.


tab monitoring


The Monitoring Tab has the following main features:

  1. Plotting buttons for viewing online feedback from a module. If you multi-select modules in the Device Tab, a plot will open for each module. The y-axis of the plots is scaled automatically. A legend for the plot is at the bottom of the plot window.

  2. DPI scaling selectors for better viewing on high-DPI displays. This setting only effects new plots that are opened.

  3. Time Window selection for controlling the rolling-window resolution of the x-axis of plots. If you select 'Auto' the x-axis will scale automatically based on the plotting frequency. This setting only effects new plots that are opened.

  4. A checkbox and slider to control Frequency that feedback is requested and plotted from a module. This setting effects all open plots.

  5. A checkbox and slider to send Commands to the selected modules in the Device Tab.

  6. A button for setting manual command Targets to a module.

For example, to check the position feedback from a module, select an appropriate device on the left panel, and then click Position plot to view the module’s reported position. Commands can be generated by enabling the Position checkbox and moving the corresponding slider. Alternatively, you can command an exact value using the corresponding target button.

If you multi-select modules in the Device Tab, a plot will open for each module. Multiple commands can be active simultaneously.

Commands are sent with a command lifetime disabled. This means that commands on a module will stay active until another command is sent. If you close Scope, commands will still remain active on a module. Unchecking the command slider will clear a given commmand. The slider inputs will not work if someone is actively commanding an actuator with command lifetime enabled.


plot position


The dashed line represents the commanded position, and the solid line the feedback position.

Note that Scope is a completely standalone process with a separate network connection that is independent from any user code. When debugging user applications we recommend running a Scope plot in the background in order to get live feedback on the behavior of an algorithm. For example, math errors often show up as large spikes or full drop-outs in the commanded line.

If you want to get an idea of the performance of the underlying netowrk the Latency and Packet loss plots are useful. The image below shows a typical latency plot for a Windows 8 computer requesting feedback at 100Hz. The green dots represent the dt between received packets and provides information about the OS scheduler. The black dots represent the round-trip time of a packet going to and from a device.


plot latency plot packet loss


The packet loss plot should ideally show a solid line at zero. If there is frequent packet loss on a wired network (Wi-Fi is expected to have some amount of packet loss and jitter) or substantial spikes in latency there may an issue. For example:

  • Bad network connection, e.g., a network cable damaged or not plugged in correctly

  • The network is saturated, e.g., someone is streaming Netflix through the same network

  • The device is overloaded, e.g., someone is sending packets faster than a device can respond (e.g. open-loop commands from the API without pause or something else that limits the request rate)

  • The host computer is overloaded, e.g., there are too many applications running in the background

Plots can be paused and zoomed by left clicking-and-dragging. Right-clicking a plot will resume updates and auto-scaling.

Left-clicking a plot will freeze it, although data may be cleared as feedback updates in the background. To freeze plotting indefinitely, uncheck the box under 'Frequency'.

Gains Tab

The gains tab provides a convenient way to change control strategies (1), set safety limits (2), and to set gains (3/4/5). Tips and guidelines on tuning gains can be found in the Motion Control section.


tab gains


Gains can be set while a device is actively being commanded as long as you are not also setting the control strategy (the dropdown box is blank). For example, you can generate commanded positions, velcoties, and efforts for an actuator programmatically (e.g. in MATLAB or C++) and use Scope to change the gains and plot the tracking performance online.

Gains are non-persisting parameters. This means that they will be reset to the last saved (persisted) values on reboot. To ensure that the current values are restored at next boot, you can press "Send & Persist" (7) or right-click persist for the selected modules on the Device Panel.

Saving/Loading Gain files

Gains can be saved in a group-compatible gain format by selecting one or more devices and pressing the save button (10). Note that the gains of multiple devices will be stored in the same order as they appear on the device pane. Thus, we recommend sorting the list in order to produce reliable results.

Loading gains (9) reads a gain file and loads the values into the corresponding input fields. Note that only one set of gains can be displayed at a single time.

Send Direct (11) loads the gains of an arbitrary number of modules and immediately sends them to the selected devices, without populating the input fields. The order will again match the order in the device pane.

Resetting Gains

In many cases it is useful to reset or compare the active gains to the default values for an actuator. An easy way to do this is by using "Load Defaults" (8), which downloads the appropriate default gains for the selected hardware type and control strategy. Differences compared to the active gains are highlighted in yellow.

Loading the default gains for a module requires an active internet connection. For cases that do not have access to the internet, we provide downloadable default gains that can be stored and loaded offline.


tab gains defaults


Device Tab

The device tab provides access to hardware features such as zeroing sensors.


tab device


The Position Reference button sets the module’s current position to the selected value. This should not be done while the device is moving

The Zero Effort Reference button sets the module’s current effort (usually torque) reading to the set value. This should be done when there is no load on the module (e.g., set down on the table with the output facing up and nothing connected to the output)

Updating Firmware

HEBI releases updates to the internal device firmware that contain performance improvements, additional features, and bugfixes. Each device has an isolated Bootloader mode, so updates can be done safely without the risk of disabling a module. You can check for updates using the Check for Firmware Updates (6) button in the top menu. Updating firmware requires an internet connection.

We strive to maintain backwards and forwards compatibility in firmware udpates. In the rare cases where we do break backwards compatibility, we will send out email notifications to all customers. If you have any concerns about a firmware update, please check the Firmware Changelog or email us at support@hebirobotics.com.


updates check


The dialog will show all modules for which updates are available. You can then select all or a subset of modules and click Update Selected. The modules will automatically reboot into Bootloader, download the appropriate application firmware, and reboot.


updates program


The update process should take about 10 seconds. Cancelling or interrupting an update will not "brick" the module, but may require you to run the update process again.



Mobile I/O

mobile io icon 512

HEBI Mobile I/O is a free app for iOS and Android that provides a way of using a mobile device to generate general-purpose input/output in the HEBI APIs. Its main feature is a touch-screen interface that mimics a game-controller layout and provides digital and analog feedback in the same format that you would read from the HEBI I/O Board. The app also exposes internal sensors, including the mobile device’s IMU, magnetometer, GPS-based location data, and on compatible devices it provides the estimated 6-DoF pose of the device using ARKit in iOS and ARCore in Android.

Getting Started

  • Get the app for free from:

  • Make sure the mobile device is connected to your local network.

  • Launch the app.

    • In order to get pose feedback based on ARKit/ARCore, you will have to allow the app to access the device camera. This is because ARKit uses the camera to track features in the world to estimate the devices full 6-DoF pose.

  • Access feedback from the mobile device in Scope or any of the APIs.

    • Matlab Examples (Github)

    • Python Examples (coming soon)

    • C++ Examples (coming soon)

App Settings

Android

In iOS the name and family of the mobile device can be set using Scope by right-clicking the mobile device on the left side panel. The name and family or any of the APIs. There are also switches in the app to enable/disable GPS location feedback and ARCore pose feedback.


iOS


ios settings


In iOS the name and family of the mobile device can be set in the settings panel for the app, found going to iOS Settings, and scrolling down the app list until you see Mobile I/O. This name and family will be how the device appears in Scope and the APIs. You can also change the layout of the iOS app to an alternative portrait Sliders view, instead of the default landscape Joystick layout.

Joystick View

virutal IO on phone


The joystick layout of the app provides 8 buttons and up to 8 analog sliders or joystick axes that you can use for input. The way the APIs communicate input from these buttons and axes are detailed in the table below. The buttons and axes are labeled on the screen. Note that the layout of the buttons and sliders may differ depending on the device.

Device Feedback

Overall, the API provides access to feedback from a mobile device the same way it does for other HEBI modules. Its main feature is a touch-screen interface that mimics a game-controller layout and provides digital and analog feedback in the same format that you would read from the HEBI I/O Board.

The Mobile I/O app also returns other sensor data, including the full 6-DoF pose of the device calculated using ARKit / ARCore on supported. The feedback availability, accuracy, and precision will depend on the device and operating system. 3

Reference Frame

Where applicable, the reference frame for the feedback from the device follows the convention of the iOS Core Motion framework (see image below).


ios core motion ref frame


I/O Feedback

Parameter Units Description

time

sec

The current time from the system clock used by the API. This is a single value that corresponds to all feedback at this timestep.

pcRxTime

sec

The system time when feedback was received by the device. The most recent of these times is what is reported as the single time above.

pcTxTime

sec

The system time when feedback requests were sent to the device.

hwRxTime

sec

The hardware timestamp when the device transmitted its feedback. Time initializes at 0 when the app is launched.

hwTxTime

sec

The hardware timestamp when the device received a request for feedback. Time initializes at 0 when the app is launched.

a1 - a8

-1 to 1

8 analog inputs based on touch input from joysticks or sliders in the app.

b1 - b8

0 or 1

8 digital inputs based on touch input from buttons in the app.

Mobile Feedback

Parameter Units Description

time

sec

The current time from the system clock used by the API. This is a single value that corresponds to all feedback at this timestep.

pcRxTime

sec

The system time when feedback was received by the decice. The most recent of these times is what is reported as the single time above.

pcTxTime

sec

The system time when feedback requests were sent to the device.

hwRxTime

sec

The hardware timestamp when the device transmitted its feedback. Time initializes at 0 when the app is launched.

hwTxTime

sec

The hardware timestamp when the device received a request for feedback. Time initializes at 0 when the app is launched.

accel or accelX accelY accelZ

m/sec^2

The device’s estimated linear 3-DoF acceleration from an internal IMU, excluding gravitional acceleration. Depending on the API, XYZ values are combined together into a single vector or returned individually.

gyro or gyroX gyroY gyroZ

rad/sec

The device’s sensed 3-DoF angular velocity from an internal IMU. Depending on the API, XYZ values are combined together into a single vector or returned individually.

magnetometer or magnetometerX magnetometerY magnetometerZ

T

The device’s sensed 3-DoF magnetic field from an internal magnetometer. Depending on the API, XYZ values are combined together into a single vector or returned individually.

altitude

m

Altitude reported by the device barometer. Not yet supported.

orientation or orientationW orientationX orientationY orientationZ

unit quaternion

The device’s 3-DoF orientation, based on Core Motion in iOS and the equivalent motion sensor APIs in Android. Depending on the API, quaternion components are combined together into a single vector or returned individually.

gpsLatitude

deg

Latitude position on the surface of the earth, as reported by the device location services, which includes GPS. Currently Android only.

gpsLongitude

deg

Longitude position on the surface of the earth, as reported by the device location services, which includes GPS. Currently Android only.

gpsAltitude

m

Altitude above sea level, as reported by the device location services, which includes GPS. Currently Android only.

gpsHeading

deg

The bearing of the horizontal direction of travel of this device, based from true north. Currently Android only.

gpsHorizontalAccuracy

m

The standard deviation of the uncertainty of the horizontal lat/long position of the device. Currently Android only.

gpsVerticalAccuracy

m

The standard deviation of the uncertainty of the vertical altitude of the device. Currently Android only.

gpsTimestamp

sec

The GPS time when feedback was received for GPS-related feedback. Currently Android only.

arOrientation or arOrientationW arOrientationX arOrientationY arOrientationZ

unit quaternion

The device’s orientation in the world, based on ARKit / ARCore. Depending on the API, quaternion components are combined together into a single vector or returned individually.

arPosition or arPositionX arPositionY arPositionZ

m

The device’s position in the world, based on ARKit / ARCore. Depending on the API, position components are combined together into a single vector or returned individually.

arQuality

0-5 or enum

Status of the tracking from ARKit / ARCore.

NaN = ArQualityNotAvailable
0 = ArQualityNormal
1 = ArQualityLimitedUnknown
2 = ArQualityLimitedInitializing
3 = ArQualityLimitedRelocalizing
4 = ArQualityLimitedExcessiveMotion
5 = ArQualityLimitedInsufficientFeatures

The values listed here are used for Scope and the Matlab API. In other APIs they are an enum.

batteryLevel

0-100

Charge level of the device’s battery (in percent).

Info Feedback

Parameter Units Description

name

string

The current user-settable name that a device shows up as in a Lookup.

family

string

The current user-settable family that a device shows up as in a Lookup.

macAddress

XX:XX:XX:XX:XX:XX

A unique identifier of the device, displayed in the format of a MAC address. This is not the actual MAC address of the device.

ipAddress

XXX.XXX.XXX.XXX

The IP Address of the device.

netMask

XXX.XXX.XXX.XXX

The network mask of the device.

gateway

XXX.XXX.XXX.XXX

The gateway of the device.

serialNumber

string

The unique identifier of the device provided by iOS.

mechanicalType

string

The class of device, e.g. iPad or iPhone X.

mechanicalRevision

string

The specific revision of the device, e.g. iPad6,11.

electricalType

string

The operating system of the device, e.g. iOS.

electricalRevision

string

The version of the operating system, e.g. 11.4.1.

firmwareType

string

The name of the app.

firmwareRevision

string

The version of the app.



MATLAB API

MATLAB is a proprietary scripting language and computing environment developed by Mathworks. It is widely used for data analysis and research across many engineering disciplines. The language’s focus on linear algebra and matrix operations makes it a very good language for programming robots.

HEBI provides libraries that enable users to control complex systems in real-time directly from MATLAB. This allows for fast prototyping and testing of algorithms on real hardware, without the need for C/C++ or code generation.

We currently do not officially support Simulink. However, we have done some preliminary work in this area. If you require need Simulink support, please contact us directly at support@hebirobotics.com.

Installation

The HEBI API for MATLAB requires MATLAB R2013b or newer.

First, download the latest Release and extract the contents into a directory of your choice. Then, open MATLAB and add the "hebi" directory to the search path.

The path can be added manually by right-clicking on the directory,

addpath

or programmatically by calling the built-in addpath function.

addpath([pwd '/hebi']);

For incorporating the HEBI library into a project, we recommend creating a startup script that can add the necessary files automatically.

function [] = startup()
% startup sets up libraries and should be started once on startup.
currentDir = fileparts(mfilename('fullpath'));
addpath(fullfile(currentDir , 'hebi'));
hebi_load(); % explicitely pre-load library
end
It is usually a good idea to increase MATLAB’s Java Heap Size using the slider control panel in Preferences→General→Java Heap Memory. In particular this will allow larger .hebilog files to be loaded into memory.

Module Discovery

The first call to HebiLookup initializes a background discovery process that can automatically discover modules using UDP broadcast messages. The default settings should be appropriate for most users, but may also be changed in hebi_config.m.

Displaying HebiLookup provides an overview of all modules that are visible on the network. The Family and Name columns show the user-settable names, and the Serial Number column shows the unique serial number of each device.

% explicit display
disp(HebiLookup);

% implicit display (no semicolon)
HebiLookup
NetworkLookup

In case you need programmatic access to details (e.g. ip address, mac address, mechanical type, etc.) of all modules, you can use the group interface.

% form a group of all modules
modules = HebiLookup.newGroupFromFamily('*');
infoTable = modules.getInfo();

Note that this can throw an error if modules are 'stale' (haven’t responded in several seconds) in which case stale modules would have to be cleared first.

HebiLookup.clearModuleList(); % reset table
pause(0.5); % allow time to re-build table

If for some reason modules are not listed, please refer to the following troubleshooting table.

Problem Suggestions

A module fails to get an IP address (LED fades orange / blue)

  • Check that the module is turned on and connected to a network with DHCP

A module did successfully get an IP address (e.g. LED fades green), but is not visible in the lookup

  • Check that the module and the computer are physically connected to the same network

  • Check that the lookup frequency is not zero, e.g., HebiLookup.setLookupFrequency(5)

  • Check that your firewall does not block incoming traffic. We have seen this issue on some Linux distributions, e.g., Scientific Linux 6, that ship with very restrictive default settings

  • Check that your network does not block UDP broadcasts. If broadcasts are blocked (e.g. on the public CMU Wi-Fi network), individual IP addresses would need to be set manually via HebiLookup.setLookupAddresses(<ip>).

The lookup does not initialize to the default settings

  • We have encountered combinations of MATLAB’s 'clear' command in combination with loading multiple Java libraries that can cause the initialization code to not run. In such cases you can manually reset to the default values with the snippet below.

% Reset lookup to default parameters and display modules
HebiLookup.initialize();
disp(HebiLookup);

If you encounter an issue connecting to modules that was not covered in the table above, please contact us directly.

Joint-Level Control

Module Selection

Modules that belong to the same system can be combined into a HebiGroup. Groups represent the basic way to send commands and retrieve feedback. They provide convenient ways to deal with modules, and handle high-level issues such as data synchronization and logging. Modules can be identified by user settable parameters, such as their name or family, or hardware constants, such as their serial number or mac address.

% Creating a group by selecting custom names
family = '2dof';
names = {'base', 'knee'};
group = HebiLookup.newGroupFromNames(family, names);
% Creating a group by selecting constant serial numbers
serials = {
    'X-00134'
    'X-00148' };
group = HebiLookup.newGroupFromSerialNumbers(serials);
The example names and serial numbers need to be modified to match devices that were found on your network during the discovery step.

A comprehensive list of all available calls for selecting modules can be found in the HebiLookup API.

Commands

Position, velocity, and effort (e.g. torque) can be commanded by using a CommandStruct. Each field expects a 1xN vector where N is the number of modules within a group. Leaving a field empty or setting a value within a vector to NaN corresponds to turning the corresponding control loop off.

CommandStruct

Commands are only valid for a limited CommandLifeTime, so even non-changing commands need to be re-sent before they expire. This safety measure may seem inconvenient at first, but it actually simplifies development due to the fact that motions automatically stop after halting a script (ctrl-c).

% Send a position command to a group with two actuators
cmd = CommandStruct();
while true
    cmd.position = [0 0];
    group.send(cmd); % immediately sends messages to all grouped modules
    pause(0.01);
end
Individual commands only set the low-level controller targets for the current instant in time, and do not calculate a trajectory from the current state to the desired target state. If you’d like to calculate a trajectory between two or more points, please take a look at the Trajectory API.

The following example shows an open-loop controller commanding sine waves with different frequencies on two actuators using simultaneous position and velocity control.

% Two actuators executing sine waves of 1Hz and 2Hz
group = HebiLookup.newGroupFromNames('leg', {'knee', 'ankle'});
cmd = CommandStruct();
w = 2 * pi * [1 2];
t0 = tic();
while true
    t = toc(t0);
    cmd.position = sin( w * t );
    cmd.velocity = cos( w * t ) * w;
    group.send(cmd);
    pause(0.001);
end

The IoCommandStruct serves as a similar structure to command the outputs on an I/O board. Note that for I/O boards we found it to be more maintainable to create a mapping and to access the individual pin fields by string as shown below.

% Pin mapping
led_r = 'c6';

% Turn on red LED
group = HebiLookup.newGroupFromNames('IO_BOARD', 'IO 25');
cmd = IoCommandStruct();
cmd.(led_r) = 1;
group.send(cmd);

Feedback

Each group continuously gathers sensor feedback from all contained modules in a background thread, and synchronizes the data into a single struct. MATLAB can then asynchronously access the received feedback with getNextFeedback. Each call returns the next new (not previously accessed) synchronized feedback. Thus, a single feedback response will never be returned more than once. The default rate of 100Hz can be changed via setFeedbackFrequency or by modifying the hebi_config.m file.

SimpleFeedback
The actual feedback frequency is fundamentally limited by the underlying operating system’s scheduler. For example, code that executes at a rate of 1KHz in Linux may be limited to ~640Hz on Windows 7. For typical rates below 250Hz this tends to be irrelevant. The Importance of Metrics and Operating Systems contains more information about the expected performance for various operating systems.
Feedback from multiple modules is typically synchronized to within 1ms. However, the actual timings depends on the network layout and external network traffic. Please see Analyzing the viability of Ethernet and UDP for robot control for more information.

Our devices tend to return a large number of sensors, so we have added different 'views' in order to enable advanced use cases without confusing new users. The default view returns feedback of the most used types of sensors, but there are alternative views in case you’d like to access additional information such as hardware timestamp data.

% Basic feedback (position, velocity, effort (e.g. torque), IMU data, temperature)
fbk = group.getNextFeedback();

% Advanced feedback including hardware timestamps
fbk = group.getNextFeedbackFull();

% I/O board feedback
ioFbk = group.getNextFeedbackIO();

The below example shows a closed-loop controller that implements a virtual spring that controls effort (torque) to drive the output towards the origin (stiffness = 1 Nm / rad).

% Virtual spring on single module
group = HebiLookup.newGroupFromSerialNumbers('<serial>');
cmd = CommandStruct();
stiffness = 1; % [Nm/rad]
while true
    fbk = group.getNextFeedback();
    cmd.effort = -stiffness * fbk.position;
    group.send(cmd);
end

While groups are completely independent of each other, there may be more than one group at a time, and multiple groups may be used together in any way. For example, the following code combines feedback from one group (gyroscope input) to calculate commands for a second group (velocity output). This is similar to the demo shown in Controlling Robots Using Android and MATLAB.

%% Setup groups
imu = HebiLookup.newGroupFromSerialNumbers('<serial>');
actuator = HebiLookup.newGroupFromSerialNumbers('<serial>');

%% Follow gyro reading using pure velocity
cmd = CommandStruct();
while true

    fbk = imu.getNextFeedback();
    cmd.velocity = fbk.gyroZ;
    actuator.send(cmd);

end

Splitting a system into multiple groups often allows for a cleaner separation between the various elements, e.g., a 6-DoF arm (group 1) and a gripper (group 2).

Further Commands

The send method additionally supports various other commands such as programmatically setting names, resetting modules, setting position limits, and setting LED colors.

LEDs can for example be used to provide live feedback to people standing next to a robot, or for synchronizing/debugging motions in combination with a high-speed camera.

group.send('led', 'red'); % set all to red
group.send('led', [0 0 1]); % set all to blue [r g b] [0-1]
group.send('led', []); % reset to default mode

Advanced Usage

The API has been developed with low-latency, and low memory usage in mind. For applications that require determinism or high control frequencies, we recommend the following additional steps.

  • Reuse command structs rather than allocate one each time in a loop

% Don't allocate CommandStruct in a loop
cmd = CommandStruct();
while true
    cmd.position = desiredPosition;
end
  • Reuse feedback structs by passing an existing struct into getNextFeedback

% Create feedback struct once and reuse it
tmpFbk = group.getNextFeedback();
while true
    fbk = group.getNextFeedback(tmpFbk);
end
  • Use convenience wrappers instead of passing parameters where possible

% Parameterized
fbk = group.getNextFeedback('view', 'full');

% Convenience wrapper (recommended)
fbk = group.getNextFeedbackFull();
  • Decrease the frequency of minor garbage collections by increasing the new space in the java.opts file. Note that this is useful for applications that need to run for extended periods of time (e.g. days/weeks) and is overkill for 99.9% of applications. It may also negatively impact e.g. live plotting.

# Minimum total heap
-Xms4g

# Maximum total heap
-Xmx4g

# Young generation heap space
-Xmn3g

Gains

The GainStruct can be used to change the gains of the control loops executed on each device.

% Double position P gain
gains = group.getGains();
gains.positionKp = gains.positionKp * 2;
group.send('gains', gains);

Gains are not automatically persisted and get reset after reboot. The following code forces a persist such that the gains are maintained during a reboot. Note that persist should not be called in a loop due to significant overhead on the device.

% Persist permanently (don't call this in a loop)
group.send('gains', gains, 'persist', true);

Each call to send results in one outgoing message per module. Thus, all commands that are combined in a single call are guaranteed to arrive at a module at the same time.

% Set gains
gains = GainStruct();
gains.positionKp = 5;

% Set desired targets
cmd = CommandStruct();
cmd.position = 0;

% Send at the same time
group.send(cmd, 'gains', gains);

Kinematics

We have created the HebiKinematics API to assist with common robotics problems such as Forward Kinematics, Inverse Kinematics, Jacobians, as well as utilities for computing joint torques and forces.

Defining the Robot Configuration

The addBody method creates a serial chain of bodies that describe the kinematic relation of a robot. A body can be a rigid link as well as a dynamic element. The first body represents the baseFrame and the last body represents the endEffector.

Details about an existing configuration can be accessed at runtime using getBodyInfo for general body information, and getJointInfo for joint specific information. The Type argument specifies the type of module or body that should be added. The currently implemented types are listed in the sections below. Parameters that are not applicable to the specified type are ignored and will not throw an error message.

Below are several sample configurations. The chain gets defined starting from the base module.

Details on parameterized body types are in the Hardware section.

X-Series Leg (2-DoF)
kin 2dof x series
% X-Series Leg (2-DoF)
kin = HebiKinematics();
kin.addBody('X5-4');
kin.addBody('X5-Link', 'ext', .216 + .0715, 'twist', 0, 'mass', 0.318);
kin.addBody('X5-1');
kin.addBody('X5-Link', 'ext', .332 + .0715/2, 'twist', 0, 'mass', 0.276);

% Relation between world frame and first body
kin.setBaseFrame(eye(4)); % expects 4x4 transform
X-Series SCARA-ish Kit (3-DoF)
kin 3dof x series SCARA ish
% X-Series SCARA-ish Kit (3-DoF)
kin.addBody('X5-4'); % 1st Module
kin.addBody('X5-LightBracket', 'mount', 'right'); % 90-deg bracket
kin.addBody('X5-9'); % 2nd Module
kin.addBody('X5-Link', 'ext', .131 + .0715, 'twist', pi/2, 'mass', 0.15); % Extension
kin.addBody('X5-4'); % 3rd Module
kin.addBody('X5-Link', 'ext', .109 + .0715, 'twist', 0, 'mass', 0.15); % Extension

% Screwdriver End Effector (X5-1 actuator)
screwdriverOut = [
    1 0 0 0
    0 1 0 0
    0 0 1 0.058
    0 0 0 1 ];

kin.addBody( 'GenericLink', ...
    'com', [0 0 0.015], ...
    'out', screwdriverOut, ...
    'mass', 0.35 );
X-Series Manipulator Kit (5-DoF)
kin 5dof x series manipulator
% X-Series Manipulator Kit (5-DoF)
kin = HebiKinematics();
kin.addBody('X5-4'); % 1st Module
kin.addBody('X5-HeavyBracket', 'mount', 'right-outside'); % 90-deg bracket
kin.addBody('X5-9'); % 2nd Module
kin.addBody('X5-Link', 'extension', 0.379, 'twist', pi); % Extension
kin.addBody('X5-9'); % 3rd Module
kin.addBody('X5-Link', 'extension', 0.252, 'twist', pi); % Extension
kin.addBody('X5-1'); % 4th Module
kin.addBody('X5-LightBracket', 'mount', 'right'); % 90-deg bracket
kin.addBody('X5-1'); % 5th Module

% Generic Gripper
gripperOut = [
    1 0 0 0
    0 1 0 0
    0 0 1 0.025
    0 0 0 1 ];

kin.addBody( 'GenericLink', ...
    'com', [0 0 0.0125], ...
    'out', gripperOut, ...
    'mass', 0.1 );

Generic Body Types

For some cases it may be necessary to add custom machined or 3D printed parts. In order to accomodate such use cases we have added support for completely generic modules.

Type Required Parameters Optional Parameters

GenericJoint

Axis

  • prismatic

    • x

    • y

    • z

  • revolute

    • rx

    • ry

    • rz

  • PositionLimit [1x2 rad]

    • default: [-inf inf]

  • VelocityLimit [1x2 rad/s]

    • default: [-inf inf]

  • EffortLimit [1x2 Nm|N]

    • default: [-inf inf]

GenericLink

  • OutputTransform [4x4]

  • CoM [x y z] in [m]

  • Mass [kg]

Generic Joint Example

Below code creates a generic joint with the same position and velocity limits as an 'S5-3' joint.

% Static S5-3 Parameters
maxPos = pi/2;
maxVel = 3.456;

% Add 'X5-3'-like joint
kin = HebiKinematics();
kin.addBody('GenericJoint', ...
    'axis', 'ry', ... % axis of movement, e.g., rotation about y axis
    'posLim',[-maxPos +maxPos], ... % position limit in [rad]
    'velLim',[-maxVel +maxVel]); % velocity limit in [rad/s]

Generic links can represent arbitrary static bodies. They are useful for specifying arbitrary end effector tools as well as custom connecting pieces.

% Generic Gripper
gripperOutput = [
    1 0 0 0
    0 1 0 0
    0 0 1 0.025
    0 0 0 1 ];
kin.addBody( 'GenericLink', ...
    'CoM', [0 0 0.0125], ... % [x y z] input to center of mass
    'OutputTransform', gripperOutput, ... % 4x4 input -> output
    'Mass', 0.1 ); % mass in [kg]

Forward Kinematics

The getForwardKinematics method computes the poses of the chain of bodies in the base frame, using specified values for the joint parameters.

Poses are returned as a set of [4 x 4 x numBodies] homogeneous transforms, specified in the world frame of the kinematic configuration. Units of XYZ translation are in [m].

% setup
% group = ...
% kin = ...

% loop
while true

    % read sensor values
    fbk = group.getNextFeedback();

    % calculate transforms from the world frame to the output frames of the individual bodies
    frames = kin.getForwardKinematics('OutputFrame', fbk.position);
    disp(frames(:,:,end)); % display end effector output

    % calculate transforms from the world frame to the centers of mass of the individual bodies
    CoM = kin.getForwardKinematics('CoMFrame', fbk.position);

    % calculate transform to only the end effector
    endEffectorFrame = kin.getForwardKinematicsEndEffector();

end

Jacobians

The getJacobian method calculates the partial derivatives of the kinematics equation, which relates the joint rates to the linear and angular velocity of each body in the kinematics configuration.

The Jacobian is returned as a [6 x numDoF x numBodies] set of matrices. Rows 1:3 of the Jacobian correspond to linear velocities [m/s] along the X-Y-Z axes in the world frame, while rows 4:6 correspond to rotational velocities [rad/s] about the X-Y-Z axes in the world frame.

The call parameters are the same as for forward kinematics shown above.

% Jacobian for all body outputs
J = kin.getJacobian('OutputFrame', fbk.position);

% Jacobian for all body centers of mass
J = kin.getJacobian('CoMFrame', fbk.position);

% Jacobian for only the end effector output frame
J = kin.getJacobianEndEffector();

Inverse Kinematics

The getInverseKinematics method computes the joint positions associated to a desired end-effector configuration. The end effector is assumed to be the last body in the kinematic chain. There are a variety of optimization criteria that can be combined depending on the application. Available parameters include:

Parameter End Effector Target Comments

XYZ

xyz position (m)

  • [3 x 1] vector for end-effector position (m)

  • Dimensions with NaN get ignored, e.g., [x y NaN] for doing IK for a planar 2-DOF arm.

TipAxis

z-axis orientation of end effector

  • [3 x 1] unit vector of target direction

SO3

3-DoF orientation of end effector

InitialPositions

Initial seed for the numerical optimization. Defaults to all zeros.

MaxIterations

Maximum allowed iterations of the numerical optimization before returning.

% Inverse kinematics on carthesian coordinates
xyz = [0 0 0];
waypoints = kin.getInverseKinematics('xyz', xyz);

% Inverse kinematics for full 6 dof
xyz = [0 0 0];
so3 = eye(3);
positions = kin.getIK('xyz', xyz, 'so3', so3);

Dynamics

Adjusting Payload

The setPayload method provides a way to dynamically specify a payload that gets used for calculating efforts to compensate for gravitational effects and joint accelerations. Specifying a payload currently has no effect on any other functionality.

% Pick up an object with the specified mass and CoM (xyz distance from the end effector output frame)
mass = 1; % [kg]
com = [0.1 0 0]; % xyz distance in [m]
kin.setPayload(mass, 'CoM', com);

% Remove payload
kin.setPayload(0);
Gravity Compensation

The getGravCompEfforts method computes efforts (torques and forces) that are required to cancel out the forces on an arm caused by gravity.

% Compensate gravity at current position
fbk = group.getNextFeedback();
gravityVec = -[0 0 1]; % Direction of gravity. Does not need to be normalized.
efforts = kin.getGravCompEfforts(fbk.position, gravityVec);

If you are not sure where gravity is coming from, an easy way to figure out the direction of gravity is to look at the accelerometer values of a fixed base module while the robot sits idle.

% automatically determine direction of gravity
fbk = group.getNextFeedback();
gravityVec = -[fbk.accelX(1) fbk.accelY(1) fbk.accelZ(1)];

% dealing with non identity base frames
baseFrame = kin.getBaseFrame();
gravityVec = baseFrame(1:3,1:3)' * gravityVec;

The following example commands a 'weightless' gravity-compensated mode that allows users to touch a robot arm and move it around freely in space. This is often useful control mode for an operator to teach waypoints for teach-repeat applications.

% Determine the direction of gravity based on the
% built-in IMU (assumes fixed base).
fbk = group.getNextFeedback();
gravityVec = -[fbk.accelX(1) fbk.accelY(1) fbk.accelZ(1)];

% Keep the robot in a weight-free teach mode
cmd = CommandStruct();
while true
    fbk = group.getNextFeedback();
    cmd.effort = kin.getGravCompEfforts(fbk.position, gravityVec);
    group.send(cmd);
end
Dynamics Compensation

The getDynamicCompEfforts method computes the efforts (torques and forces) that are required to accelerate the body masses as determined from the specified positions, velocities, and accelerations.

The 'Position' argument expects a vector of positions of all degrees of freedom, used for computing the Jacobian, where effort = J' * desiredForces.

'TargetPositions', 'TargetVelocities', and 'TargetAccelerations' typically come from some sort of trajectory generation function, such as the TrajectoryGenerator API below.

% Calculate required efforts to compensate for desired accelerations
efforts = kin.getDynamicCompEfforts(...
    fbk.position, ...
    cmdPositions, ...
    cmdVelocities, ...
    cmdAccelerations);

Trajectories

We provide trajectories via the HebiTrajectoryGenerator API to control a group of actuators in a manner similar to more traditional robot arms. Features include parameterized minimum-jerk motion, and built-in gravity and dynamics compensation using the kinematics API.

Blocking Trajectories

The moveJoint and moveLinear functions are simplified wrapper calls that respectively move joints in joint space, or such that the end effector path resembles a straight line in world coordinates. Both calls block until the motion is completed.

% Setup
trajGen = HebiTrajectoryGenerator(kin);

% Move a group of 3 actuators between two waypoints
start = [0 0 0]; % [rad]
finish = pi/2 * [1 1 1]; % [rad]
trajGen.moveJoint(group, [start; finish]);

The default settings use only position and velocity control. Effort control can be enabled with optional parameters.

% Enable effort control
trajGen.moveJoint(group, [start; finish], ...
    'GravityVec', gravityVec, ...
    'DynamicsComp', true);

Gravity compensation or dynamics compensation should always be used together. Otherwise the target efforts may be too low and interfere with position/velocity control, which depending on the control strategy could make the performance worse.

Using these wrapper calls is equivalent to creating a non-blocking trajectory and calling the blocking executeTrajectory function. If you need access to more advanced trajectory options, you can manually split the calls up as shown below.

% Create non-blocking trajectory (offers more options)
traj = trajGen.newJointMove(positions);

% Execute and return when motion is done (blocking)
trajGen.executeTrajectory(group, traj);

Non-Blocking Trajectories

There are many use cases for which it is important to dynamically react to changes in the environment such as hitting unforeseen obstacles or when following a moving target. This can be done by creating a trajectory (newJointMove or (newLinearMove) and executing it manually.

% Setup single joint
group = HebiLookup.newGroupFromNames('*', 'X-001');
kin = HebiKinematics();
kin.addBody('X5-1');
trajGen = HebiTrajectoryGenerator(kin);
trajGen.setAlgorithm('UnconstrainedQp'); % MinJerk trajectories
trajGen.setSpeedFactor(0.5); % Slow down to half of max speed

% Create trajectory through multiple waypoints
nWaypoints = 5;
positions = rand(nWaypoints, kin.getNumDoF());
trajectory = trajGen.newJointMove(positions);

% Execute trajectory in position and velocity
cmd = CommandStruct();
t0 = tic();
t = toc(t0);
while t < traj.getDuration()
    t = toc(t0);

    % React to something (e.g. position error or effort threshold)
    fbk = group.getNextFeedback();
    if fbk.position - fbk.positionCmd > 0.1
        group.send(CommandStruct()); % turn off commands
        error('Reacting to something...');
    end

    % Get target state at current point in time
    [pos, vel, accel] = traj.getState(t);

    % Command position/velocity
    cmd.position = pos;
    cmd.velocity = vel;
    group.send(cmd);

end

More information on various options, such as

  • Algorithm

  • Speed factor

  • Minimum duration

  • Total duration

  • Time constraints

  • Velocity constraints

  • Acceleration constraints

can be found in the API documentation for HebiTrajectoryGenerator and newJointMove.

Visualizing Trajectories

The following code creates a sample trajectory between 6 position waypoints for a single actuator.

% Setup
kin = HebiKinematics();
kin.addBody('X8-3');
trajGen = HebiTrajectoryGenerator(kin);

% Create trajectory
positions = [0 pi 3 2.5 1 0];
time = [0 1 2 3 4 5];
trajectory = trajGen.newJointMove(positions, 'time', time);
single joint trajectory

The resulting trajectory can be visualized using standard MATLAB plots as shown below.

% Visualize trajectory
figure(); hold on; grid on;

% Position / Velocity / Acceleration profile
t = 0:0.01:trajectory.getDuration();
[pos, vel, accel] = trajectory.getState(t);
plot(t, pos);
plot(t, vel);
plot(t, accel);

% Superimpose target position waypoints
tWaypoint = trajectory.getWaypointTime();
plot(tWaypoint, trajectory.getState(tWaypoint), 'bo');

% Axes labels
title('Joint Trajectory');
ylabel('Value [rad, rad/s, rad/s^2]');
xlabel('Time [s]');
legend Position Velocity Acceleration

Logging

Data logging, visualization, and analysis tools are a critical part of robotics development. We provide powerful logging capabilities that allow the logging of data in excess of 1kHz, from any number of modules, and over extended periods of time (multiple days). Logging is done entirely outside of the main MATLAB thread, written directly to disk, so that there is no performance impact on real-time control. This also means that in case MATLAB ever crashes due to unforeseen circumstances, none of the logged data is ever lost.

The logging format is also common across all HEBI APIs, so logs saved with by one API can read by any of the other APIs.

% Log data for 5 seconds
group.startLog();
pause(5);
data = group.stopLog();

Logs are returned as structures where data is organized in vectors and matrices so that the resulting data interacts well with MATLAB’s built-in plotting tools.

% Plot logged data and overlay commanded and feedback position
figure(); hold on;
plot(data.time, data.position);
plot(data.time, data.positionCmd, ':');
The default stopLog call returns a Java object that contains all the data. Thus, each access needs to convert the underlying Java type to the corresponding MATLAB type. This is typically not an issue, but if you need to access fields many times, we recommend converting the Java object to a MATLAB struct via struct(data).

Loading logs from file

All logging happens in the background and gets stored on disk in a binary streaming format. The default stopLog() call converts the binary format into another format that MATLAB can work with. We also provide the convertGroupLog() utility to load raw log files that were previously saved or generated using other APIs.

% load raw log file
binaryFile = '2015-12-02_17-02-12.994887.hebilog';
matFile = HebiUtils.convertGroupLog(binaryFile, 'format', 'mat', 'view', 'full');
data = load(matFile);

Logging over extended periods of time

The same utility also useful for splitting logs into e.g. 1 hour chunks in a long running process. The conversion would be too expensive to do inside a loop, so instead we can call startLog to finish the previous log and to start a new one. This is comparatively very cheap and may be called inside a tight loop.

% log continously over an extended period of time
binaryFiles = {};
t0 = tic();
while true

    % run control loop
    % ...

    % create a new log every hour
    if toc(t0) > 3600
        binaryFiles{end+1} = group.startLog();
        t0 = tic();
    end

end

The resulting log files can then be loaded once a task is done, or from a different MATLAB instance while the task is still running.

% convert log files offline
nFiles = length(binaryFiles);
for i = 1:nFiles
    disp(['converting log ' num2str(i) '/' num2str(nFiles)];
    matFile = HebiUtils.convertGroupLog(binaryFile, 'format', 'mat', 'view', 'full');
end

Replaying logs for testing and verification

We also added the newGroupFromLog utility which supports replaying log files via the standard group interface. After loading the log, each call to getNextFeedback on the group returns the next feedback in sequence (including the logged timestamp), regardless of the rate it is actually being called. This is useful, for example, for online algorithm development while working with offline data.

% Iteratively step through log file
group = HebiUtils.newGroupFromLog(path);
while true
    fbk = group.getNextFeedback();
    disp(fbk.position);
end



Python API

Examples for the Python API can be found on our GitHub account. The latest version of the API reference can be found here. This guide also provides many hyperlinks to the API reference where relevant.

The examples are divided into a few folders: basic, advanced, and kits. The basic examples are designed as a hands-on tutorial for getting started with the API; we highly recommend downloading and going through these examples as concrete/runnable examples of the concepts discussed in this documentation.

Installation/Project Integration

The HEBI Python API is available through PyPI, which requires the use of pip.

The API requires numpy, which is available on practically any platform Python is supported. For some functions in the hebi.util module, matplotlib is required; however, matplotlib is not a hard requirement since it may not be available on machines without a graphical user interface (e.g., a Linux machine without an X server).

While not a requirement for the HEBI Python API itself, some of the kit examples (e.g., igor) also require some SDL libraries:

The GitHub examples repository provides the SDL2 library for Windows users. It is automatically loaded when necessary on Windows, but not for Linux and MacOS. Linux and MacOS users must install it themselves. Debian/Ubuntu users can install it using sudo apt-get install libsdl2-2.0-0 and Fedora/RHEL users can install it using sudo [yum|dnf] install SDL2.

Installing from the command line

From the command line, you can download the API from pip:

# On Linux, it is recommended to install to the user directory
pip install --user hebi-py
# Other platforms (e.g., Windows, MacOS)
pip install hebi-py

Installing from IDEs

Most modern IDEs (e.g., PyCharm) have pip integration by default. You can use the provided facilities to find the hebi-py package and install it.

Remarks

Some Linux distributions prefer you to install numpy through their package manager. For example, Ubuntu prefers for you to use python-numpy or python3-numpy for numpy. Fedora prefers python2-numpy or python3-numpy.

For some Linux environments, you may not be able to easily install matplotlib because you do not have a desktop environment (e.g., running a server Linux install on a Raspberry Pi). For such environments, you cannot use the plotting functions under hebi.util, but the rest of the API will not be affected.

Module Discovery

The Python API provides an easy to use facility to discover and communicate with HEBI modules on network interfaces. All discovery will occur through the The Lookup class. The Lookup class manages a background discovery process that can find modules on the local network using UDP broadcast messages. This class acts as a singleton and can be disposed of after creating the desired The Group objects.

To view the modules that have been discovered, use the EntryList class to programmatically access the contents at a snapshot in time.

from time import sleep
lookup = hebi.Lookup()
# Give the Lookup process 2 seconds to discover modules
sleep(2)
print('Modules found on network:')
for entry in lookup.entrylist:
  print('{0} | {1}'.format(entry.family, entry.name))

Depending on the available modules, the output will look similar to

Broadcasting on:
  10.10.10.255

Modules found on network:
arm-1 | base
arm-1 | shoulder
arm-1 | elbow
platform | left-wheel
platform | right-wheel

Joint-Level Control

Module Selection

A Lookup object can create a Group instance. A group is a collection of modules (often part of the same robotic system), and the group interface represents the basic way to send commands and retrieve feedback. They provide convenient ways to deal with modules and handle high-level issues such as data synchronization and logging. Modules can be identified by user settable parameters, such as name or family, or hardware constants, such as their mac address.

# Create a group from a set of names
group = lookup.get_group_from_names(['Family'], ['name1', 'name2'])
# Can provide a different family for each module
families = ['mobile_base', 'mobile_base', 'arm', 'arm']
names = ['left_wheel', 'right_wheel', 'shoulder', 'elbow']
group = lookup.get_group_from_names(families, names)
# Providing '*' as the family selects all modules
group = lookup.get_group_from_family('*')

The Lookup class documentation provides a comprehensive set of functions used to create group objects.

Commands

Position, velocity, effort, and many other fields can be commanded by using the Command API. A GroupCommand object is used to store the commands to be sent to the group. We use types from the numpy library to update many of the fields with vectors of data.

# numpy is aliased to np throughout this documentation.
import numpy as np

# Create a numpy array filled with zeros
efforts = np.zeros(group.size)

# Command all modules in a group with this zero force or effort command
group_command = hebi.GroupCommand(group.size)
group_command.effort = efforts
group.send_command(group_command)
Setting the values in the GroupCommand object does not affect/command any modules until the command is sent to the group with the http://docs.hebi.us/docs/python/0.99.0/hebi/py_api.html#hebi.internal.group.Group.send_command[Group.send_command] function. Also note that commands can be dropped on particularly poor or congested networks, and the function does not provide guaranteed delivery or even an acknowledgement; it is designed for relatively high-frequency applications where commands are resent frequently (_e.g., 100Hz).

For commands that are only sent once (such as setting gains on the modules), we recommend usage of Group.send_command_with_acknowledgement to verify that the command was received.

The command affects the actuator control loops only if at least one of the position, velocity, and effort command fields is not cleared. In particular, a command sent with all three of these cleared will not affect the control loop setpoints. However, if only a subset of the fields are set (e.g., only position), then commands from the other fields will be cancelled when this message is receieved.

If you want to completely cancel out a previous command (especially when the Command Lifetime is not set!) and turn the control loops off, you can set the commanded value to nan. A few ways to get a nan in Python are:

# parse from string
a = float('nan')
# numpy provides it as an attribute
b = np.nan
Individual commands only set the low-level controller targets for the current instant in time, and do not calculate a trajectory from the current state to the desired target state. In other words, setting a single position command will result in a step response from the actuator. If you’d like a smooth motion between two or more positions, please take a look at the trajectory API.

The individual Command elements of a GroupCommand object can also be accessed and updated individually. Note that you cannot create an instance of Command - you only retrieve references through a GroupCommand. The Command object provides the same interface as a GroupCommand, but with all of the fields returning a scalar as opposed to a vector or matrix. As an example, updating the name of the module in the following example:

group_command = hebi.GroupCommand(group.size)
command = group_command[0] # Retrieve reference to the command for first module
command.name = "new name"
# Only changes the name for the first module
group.send_command_with_acknowledgement(group_command)
Command Lifetime

Commands are only valid for a limited command lifetime, so even unchanging setpoints need to be re-sent before they expire. When set, this means that is a process is killed (e.g., with ctrl-c), commanded motions are aborted. This is set on a per-group basis with the Group.command_lifetime field. By default, the command lifetime is 100ms.

# Sets command timeout to 100 milliseconds
group.command_lifetime = 100.0
# Command must be sent in loop at a faster rate than the lifetime in order to remain in effect.
while not (stop_loop):
  group.send_command(group_command)
  sleep(0.05)
Example Use Cases

The following example shows an open-loop controller commanding sine waves with different frequencies on two actuators using simultaneous position and velocity control.

...

group = lookup.get_group_from_names(["knee", "ankle"], ["leg"])
group.command_lifetime = 20.0

group_command = hebi.GroupCommand(group.size)

w = np.array([math.pi*2.0, math.pi*4.0], dtype=np.float64)
w_t = np.empty(2, dtype=np.float64)
t = 0.0
dt = 0.01 # 10 ms

while not (stop_loop):
  np.multiply(w, t, w_t)
  group_command.position = np.cos(w_t)
  group_command.velocity = np.sin(w_t)
  group.send_command(group_command)

  sleep(dt)
  t = t + dt

Note in addition to actuator commands such as position, velocity, and effort, the Command object can be used to set values for other types of modules. Here, we set the value on an I/O board digital output pin to 'high'.

group = lookup.get_group_from_names(["IO_BOARD"], ["sensor interface"])
group_command = hebi.GroupCommand(group.size)

group_command.io.e.set_int(1) # broadcasts 1 to all "e" IO pins in group
group.send_command(group_command)

Feedback

A group is responsible for gathering sensor feedback from all contained modules, and synchronizing these into a single GroupFeedback structure. By setting the group’s feedback frequency (set Group.feedback_frequency), a background thread will request and synchronize this feedback at a specified rate. The default feedback rate is 100Hz.

The actual feedback frequency is fundamentally limited by the underlying operating system’s scheduler. For example, code that executes at a rate of 1KHz in Linux may be limited to ~640Hz on Windows 7. For typical rates below 250Hz this tends to be irrelevant. The Importance of Metrics and Operating Systems contains more information about the expected performance for various operating systems.
Feedback from multiple modules is typically synchronized to within 1ms. However, the actual timings depends on the network layout and external network traffic. Please see Analyzing the viability of Ethernet and UDP for robot control for more information.

To access this feedback, there are three options:

Get Next Feedback

The first is to call Group.get_next_feedback. This will return the next new (not previously accessed) synchronized feedback. Thus, a single feedback response will never be returned more than once. If there is no new feedback since the last call to this function, it will block and wait for a new packet (up to a configurable timeout), so this can be used as an easy way to control the rate of a loop. For example:

# Best practice is to allocate this once, not every loop iteration
group_feedback = hebi.GroupFeedback(group.size)

# This effectively limits the loop below to 200Hz
group.feedback_frequency = 200.0

while not (stop_loop):
  fbk = group_feedback
  group_feedback = group.get_next_feedback(reuse_fbk=group_feedback)
  if (group_feedback == None):
    group_feedback = fbk
    continue

  # ... read/use feedback object contents here.

This use of Group.get_next_feedback parallels the approach taken in the HEBI API for MATLAB.

Feedback Callbacks

The second way to access this feedback is to add a feedback handler that is called each time new synchronized group feedback is available. The callback will execute on a background thread. Below we show two methods for adding a callback function

# Function feedback callback
def feedback_func_callback(feedback):
  # feedback is guaranteed to be a "hebi.GroupFeedback" instance
  # ... read/use feedback object contents here


# Sets frequency to 200 Hz
group.feedback_frequency = 200.0

# Add feedback handler
group.add_feedback_handler(feedback_func_callback)
Manual Feedback Requests

You can also retrieve feedback when the feedback frequency is zero by manually sending a feedback request. This allows finer grained control over the network traffic that is sent.

group_feedback = hebi.GroupFeedback(group.size)
# Sends a request to the modules for feedback and immediately returns
group.send_feedback_request()
# Blocks until feedback is in the background queue or timeout
group_feedback = group.get_next_feedback(reuse_fbk=group_feedback)
Setting the feedback frequency through the feedback_frequency field effectively calls the send_feedback_request function at a certain rate on a background thread.
Feedback Contents

The GroupFeedback object contains position, velocity, and effort actuator feedback, as well as sensor feedback from a number of sensors. Similar to commands, many of the values can be accessed as numpy vectors or matrices, with one row for each module in the group.

# Fill in feedback
group_feedback = hebi.GroupFeedback(group.size)
group_feedback = group.get_next_feedback(reuse_fbk=group_feedback)

# Retrieve positions:
positions = group_feedback.position
print('Position Feedback:\n{0}'.format(positions))

# Get Gyro
gyros = group_feedback.gyro
print('Gyro Feedback:\n{0}'.format(gyros))

# Individually access io pin feedback from first module
io_a = group_feedback.io.a
if (io_a.has(1)):
  print('PinA: {0}'.format(io_a.get_int(1)[0]))
Use Case Example

The below example shows a closed-loop controller that implements a virtual spring that controls torque to drive the output towards the origin (stiffness = 1 Nm / rad).

# Virtual spring
lookup = hebi.Lookup()
# change to your name(s)/family(s)
group = lookup.get_group_from_names(["name"], ["family"])
command = hebi.GroupCommand(group.size)
stiffness = 1.0 # [Nm/rad]

feedback = hebi.GroupFeedback(group.size)

while not (stop_loop):
  # wait for the feedback
  feedback = group.get_next_feedback(reuse_fbk=feedback)
  command.effort = feedback.position * -stiffness
  group.send_command(command)

Using a lambda callback, this can be rewritten as:

# Virtual spring
lookup = hebi.Lookup()
# change to your name(s)/family(s)
group = lookup.get_group_from_names(["name"], ["family"])
command = hebi.GroupCommand(group.size)
stiffness = 1.0 # [Nm/rad]

def fbk_handler(feedback):
  command.effort = feedback.position * -stiffness
  group.send_command(command)

group.add_feedback_handler(fbk_handler)

Best Practices

Performance and Efficiency

The API has been developed with low latency and high efficiency in mind. For applications that require high control frequencies, we recommend the following additional steps.

  • Reuse Command and Feedback objects rather than allocating one each time in a loop

from time import sleep

# allocate group_command and group_feedback outside tight loop below
group_command = hebi.GroupCommand(group.size)
group_feedback = hebi.GroupFeedback(group.size)

# Set maximum loop speed
fbk_frequency = 100.0
group.feedback_frequency = fbk_frequency

while not (stop_loop):
  # Save reference to feedback object in case get_next_feedback returns None
  fbk = group_feedback

  # Wait for the feedback
  group_feedback = group.get_next_feedback(reuse_fbk=group_feedback)
  if (group_feedback == None):
    group_feedback = fbk
    continue

  pos = group_feedback.position

  # edit "pos"
  ...

  # set command data
  group_command.position = pos
  group.send_command(group_command)

  sleep(1.0/fbk_frequency)

Gains

Control loop gains can be retrieved and set on each device by features provided by the Command and Info APIs.

The recommended way to set and load gains is to use the XML gain format that works across HEBI APIs and GUI tools.

# Saving gains from a group of modules:
group_info = group.request_info()

if (group_info != None):
  group_info.write_gains("saved_gains.xml")

# Loading gains from a file onto a group of modules:
group_cmd = hebi.GroupCommand(group.size)
group_cmd.read_gains("saved_gains.xml")
group.send_command_with_acknowledgement(group_cmd)

Alternatively, individual gains can be accessed and set programmatically. You will see that there are associated gains fields within both the GroupCommand and GroupInfo classes.

The example below demonstrates programmatic doubling of the position P gain for each module in the group:

# Double position P gain
group_info = hebi.GroupInfo(group.size)
group_command = hebi.GroupCommand(group.size)

# Reuse "group_info" for best practice
group_info = group.request_info(group_info)

group_command.position_kp = group_info.position_kp * 2.0

group.send_command(group_command)

Robot Model / Kinematics

The hebi.robot_model module contains classes and functions to store and work with robot kinematic configurations. These assist with computing forward and inverse kinematics and Jacobians.

Defining the Robot Configuration

The robot_model.RobotModel class is used to store how different rigid bodies and joints are attached together. There are helper functions to add standard HEBI components to the robot model.

The first body added to the RobotModel object represents the base and the last body represents the end (often an end effector) of the kinematic chain.

Currently the API assumes a serial kinematic chain of bodies. Support for kinematic trees is planned in a future release.

Various factory methods are used to add HEBI components to the robot model; these fall into 3 basic categories: Actuator, Link, and Bracket:

# Create a simple kinematic description of a 3-DOF robot arm
model = hebi.robot_model.RobotModel()
model.add_actuator('X5-4')
model.add_bracket('X5-LightBracket', 'right')
model.add_actuator('X5-4')
model.add_link('X5', 0.18, math.pi)
model.add_actuator('X5-4')
model.add_link('X5', 0.28, 0.0)

The complete list of these methods is documented here. These methods and their named parameters correspond with the actuator, link, and bracket types given in the Kinematic Body Types section.

In addition to standard HEBI components, you can use the RobotModel.add_rigid_body and RobotModel.add_joint functions to add any desired masses and degrees of freedom to your system.

Forward Kinematics

The get_forward_kinematics method computes the poses of each body in the RobotModel object, using specified values for the joint parameters. Using the frame_type parameter, one can choose either the center of mass or the output frame for each kinematic body.

The resulting 4x4 homogeneous transforms are specified in the world frame of the RobotModel object. This world frame defaults to the input frame of the base module, although a transformation can be prepended using the base_frame field. Units of translation are in meters.

# First, build up a hebi.robot_model.RobotModel object, as in previous examples.
...

# These are the current joint angles/positions, perhaps from a GroupFeedback object:
angles = group_feedback.position
transforms = model.get_forward_kinematics('output', angles, transforms)

for i, transform in enumerate(transforms):
  print('Transform for module {0} is\n{1}\n'.format(i, transform))

The RobotModel.get_end_effector method is a shortcut for just obtaining the output transform of the last frame in the RobotModel object:

# Set up hebi.robot_model.RobotModel object "model" and np.ndarray "angles" as above...
...
transform = model.get_end_effector(angles)
print("4x4 transform from base to end effector: {0}".format(transform))

Jacobians

We can also use the RobotModel object to obtain the Jacobian, or the partial derivatives of the forward kinematics equation. This relates the joint velocities to the linear and angular velocity of each body in the system.

The Jacobian in returned as a set of matrices, one for each body in the RobotModel object. Each matrix is of size 6 x numDoF. Rows 1 - 3 of this matrix corresponds to this body’s linear velocities [m/s] along the X-Y-Z axes in the world frame, while rows 4 - 6 correspond to rotational velocities [rad/s] about the X-Y-Z axes in the world frame. The columns define the contributions to these linear and rotational velocities that would be a result of the respective degree of freedom instantaneously moving at unit velocity (e.g., a rotational joint being driven at 1 rad/s).

As with the forward kinematics, we provide versions for the end effector (which just returns a since 6 x numDoF matrix) as well as the entire system.

# Set up hebi.robot_model.RobotModel object "model" and np.ndarray "angles" as above...
...

# Get Jacobian for all bodies:
transforms = model.get_jacobians('output', angles)

# Get Jacobian for end effector:
# 'reuse_jacobians' is an optional argument, but using it to provide
# a previously computed jacobians list amortizes the time to call this function
transform = model.get_jacobian_end_effector(angles, reuse_jacobians=transforms)

Full API documentation of these functions is available here.

Inverse Kinematics

The RobotModel object also provides a method for computing joint angles that solve the inverse kinematics, according to a number of objectives and constraints.

Note that the implementation of this function relies on a local optimization, and so is highly dependent on the initial joint angles that are provided as the seed for the search.

The desired objectives and constraints should be passed to the RobotModel.solve_inverse_kinematics function as a series of arguments - any number are permitted, but there are limitations in which can be used together.

As an example, the solve_inverse_kinematics function is used to find joint angles that move the end effector to a given position:

# Set up RobotModel object "model" and vector "angles" as above...
...

target_xyz = [0.4, 0.0, 0.2]
initial_joint_angles = np.array(model.dof_count)

# Fill in 'initial joint angles', perhaps with feedback from the robot.
...

end_effector_position_objective = hebi.robot_model.endeffector_position_objective(target_xyz)

################################################################################
# Demonstration of using a single objective:
################################################################################

ik_result_joint_angles = model.solve_inverse_kinematics(initial_joint_angles,
  end_effector_position_objective)

################################################################################
# Demonstration of using a custom objective:
################################################################################

def custom_objective_func(positions, errors, user_data):
  # This objective prefers joint angles that sum to '2'
  errors[0] = 2.0 - positions.sum()

user_data = None # can be anything

custom_objective = hebi.robot_model.custom_objective(1, custom_objective_func, user_data)

ik_result_joint_angles = model.solve_inverse_kinematics(initial_joint_angles,
  custom_objective)

################################################################################
# Demonstration of using a multiple objectives:
################################################################################

# Set joint limit constraints:
min_positions = [-math.pi, -1.0, -1.0]
max_positions = [math.pi, 1.0, 1.0]

joint_limit_constraint = hebi.robot_model.joint_limit_constraint(min_positions,
  max_positions)

ik_result_joint_angles = model.solve_inverse_kinematics(initial_joint_angles,
  end_effector_position_objective,
  joint_limit_constraint)

Full API documentation of these functions is available here.

Gravity Compensation

Currently, computation of torques for gravity compensation is not supported directly in the API, but our examples repository provides a utility function and examples for adding this into your own system. This is subject to change and inclusion directly in the API in the future. This example in c++ demonstrates sending torques to compensate for the effect of gravity for a 3 DOF arm.

Trajectories

A trajectory is a (smooth) motion through two or more waypoints (a waypoint is a specific joint positions for each module in the robot). A trajectory defines the position, velocity, and acceleration for each module at each point in time, from the start to end of the trajectory.

More traditional robot arms usually are controlled by defining waypoints and some motion parameters (e.g., maximum acceleration), as opposed to the joint-level control our API provides at the lowest level. The trajectory API provides a way to define motions in a similar high-level way as these traditional systems, while still allowing access to control and feedback at a lower level.

Generating Trajectories

The Trajectory class is used to generate and access trajectory motions. To create a trajectory object, you must define the waypoints and time at which the robot should pass through each waypoints. Note that the waypoint velocity and acceleration constraints are optional, but it is often useful to at least define the starting and ending velocity and acceleration to be zero. If these are not given, it is left up to the internal implementation to determine values for these.

Below, we demonstate creation of a trajectory for 2 modules over 5 waypoints, using the unconstrained quadratic programming solver:

# Position, velocity, and acceleration waypoints.
# Each column is a separate waypoint.
# Each row is a different joint.

pos = np.empty((num_joints, 5))
vel = np.empty((num_joints, 5))
acc = np.empty((num_joints, 5))

# Set first and last waypoint values to 0.0
vel[:,0] = acc[:,0] = 0.0
vel[:,-1] = acc[:,-1] = 0.0
# Set all other values to NaN
vel[:,1:-1] = acc[:,1:-1] = np.nan

# Set positions
pos[:,0] = current_position[:]
pos[:,1] = 0.0
pos[:,2] = [math.pi*2.0, 0.0]
pos[:,3] = [0.0, -math.pi*2.0]
pos[:,4] = 0.0

# The times to reach each waypoint (in seconds)
time = np.linspace(0.0, 20.0, 5.0)

# Define trajectory
trajectory = hebi.trajectory.create_trajectory(time, positions, velocities, accelerations)

Executing Trajectories

To play through the trajectory object, we query the position, velocity,' and acceleration at each point in time, and send appropriate commands to the robot:

from time import sleep

# Follow the trajectory
cmd = hebi.GroupCommand(num_joints)
period = 0.01
duration = trajectory.duration

pos_cmd = np.array(num_joints, dtype=np.float64)
vel_cmd = np.array(num_joints, dtype=np.float64)

t = 0.0

while (t < duration):
  pos_cmd, vel_cmd, acc_cmd = trajectory.get_state(t)
  cmd.position = pos_cmd
  cmd.velocity = vel_cmd
  group.send_command(cmd)

  t = t + period
  sleep(period)

Note that acceleration can not be (meaningfully) directly sent to the actuator, but with knowledge of the dynamics this can be converted to a torque or force to compensate for this acceleration. For improved control, we recommend adding effort commands that are the summation of torques/forces that compensate for gravity and torques/forces that compensate for this acceleration.

Logging

Data logging, visualization, and analysis tools are a critical part of robotics development. Our logging capabilities allow logging of data from groups of modules in excess of 1kHz, and over extended periods of time (multiple days). Logging is done on a background thread so there is no impact on the control logic, and logging easy to enable/disable, even in complex existing code.

The logging format is common across all HEBI APIs, so logs saved with one API can be read by any other API.

Here is a simple example of logging data. The logging occurs at the group’s feedback frequency.

# get "group" from lookup
...

# Start logging (note that extension is needed)
log_file_location = group.start_log('/path/to/output', 'filename.hebilog')

# main control logic here
...

# Stop logging
group.stop_log()

Additionally, you can optionally provide the directory and/or file name for the log file like:

# Will start a log in the process' current working directory, with
# a name generated using the timestamp of the current time and date
log_file_location1 = group.start_log()

# Will generate a name in the provided directory
log_file_location2 = group.start_log(directory='/path/to/output')

The log data is stored in a file on disk. The name is returned with the initial Group.start_log call, and is also retrievable through the LogFile object returned when stopping the log.

The returned LogFile object allows sequential access to the content in the log file. The below example demonstrates analysing a logfile to find the highest torque experienced by each joint:

num_modules = log_file.number_of_modules
feedback = hebi.GroupFeedback(num_modules)

# If we are trying to find the max, start with the lowest possible number.
max_efforts = np.array([-np.inf] * num_modules, dtype=np.float64)

# Reuse feedback as a best practice
feedback = log_file.get_next_feedback(feedback)
# Get the component-wise max each feedback entry compared to the running max efforts
while (feedback != None)
  max_efforts = np.maximum(max_efforts, feedback.position)
  feedback = log_file.get_next_feedback(feedback)

Note that you can also manually create a LogFile object from an existing file:

log_file = hebi.util.load_log('/path/to/file.hebilog')



C++ API

Note: this documentation refers to the C++ 1.0 API, available at:

Also available are earlier versions of the API and Doxygen documentation.

The examples are divided into two folders: basic and advanced. The basic examples are designed as a hands-on tutorial for getting started with the API; we highly recommend downloading and going through these examples as concrete/runnable examples of the concepts discussed in this documentation.

Installation/Project Integration

The C++ API is made up of:

  1. The .cpp and .hpp files that comprise the source code of the C++ wrapper around the underlying C library.

  2. The hebi.h header file that provides the function declarations for the underlying C library.

  3. The actual dynamic/shared object library that must be linked into your program (.dll for Windows, .so for Linux, and .dylib for macOS).

The C++ API is designed to be used with the C++ 11 standard; when building your project, ensure you have set the proper compiler switches to enable this support (e.g., -std=c++11 for gcc).

There are a few basic ways to integrate the API into your program:

  1. If you are using Visual Studio, use the NuGet package manager to add the HEBI C++ API as a dependency. You will have to disable "precompiled header" support for the API to properly compile with your project.

  2. Use the examples as a template for setting up a CMake or Visual Studio project dependent on the C++ API.

  3. Add the source .cpp files to your project sources, ensure your include path contains the .hpp and hebi.h file, and link in the shared object for your platform and architecture.

Module Discovery

The hebi::Lookup class manages a background discovery process that can find modules on the local network using UDP broadcast messages. Generally, you will only need one hebi::Lookup object per application, and it can be disposed of after creating the desired hebi::Group objects (see Module Selection).

To view the modules that the Lookup has discovered, use the hebi::LookupEntry class to programmatically access the contents at a snapshot in time.

// Create Lookup and wait for it to populate
hebi::Lookup lookup;
std::this_thread::sleep_for(std::chrono::seconds(1));

// Take snapshot and print to the screen
auto entry_list = lookup.getEntryList();
std::cout << "Modules found on network (Family | Name):" << std::endl;
for (auto entry : entry_list)
{
  std::cout << entry.family_ << " | " << entry.name_ << std::endl;
}

Depending on the available modules, the output will look similar to:

Broadcasting on:
  10.10.10.255

Modules found on network (Family | Name):
arm-1 | base
arm-1 | shoulder
arm-1 | elbow
platform | left-wheel
platform | right-wheel

If for some reason modules are not listed, please refer to the following troubleshooting table.

Problem Suggestions

A module fails to get an IP address (LED fades orange / blue or orange / green)

  • Check that the module is turned on and connected to a network with DHCP

A module did successfully get an IP address (e.g. LED fades green), but is not visible in the lookup

  • Check that the module and the computer are physically connected to the same network

  • Ensure that the hebi::Lookup object was created after the network interface connected to the network; for simple programs, one can just restart the program after connecting to the network

  • Check that your system firewall does not block incoming traffic. We have seen this issue on some Linux distributions, e.g., Scientific Linux 6, that ship with very restrictive default settings

  • Check that your network does not block UDP broadcasts

If you encounter an issue connecting to modules that was not covered in the table above, please contact us directly.

Joint-Level Control

Module Selection

A Lookup class can create a hebi::Group instance. A group is a collection of modules (often part of the same robotic system), and the group interface represents the basic way to send commands and retrieve feedback. They provide convenient ways to deal with modules and handle high-level issues such as data synchronization and logging. Modules can be identified by user settable parameters, such as name or family, or hardware constants, such as their mac address.

// Create a group from a set of names
std::shared_ptr<Group> group = lookup.getGroupFromNames({"family"}, {"name1", "name2"});
// Can provide a different family for each module
std::vector<std::string> families = {"mobile_base", "mobile_base", "arm", "arm"};
std::vector<std::string> names = {"left_wheel", "right_wheel", "shoulder", "elbow"};
std::shared_ptr<Group> group = lookup.getGroupFromNames(families, names);
// providing "*" as the family selects all modules
std::shared_ptr<Group> group = lookup.getGroupFromFamily("*");

The Lookup class documentation provides a comprehensive set of functions used to create group objects.

Commands

Position, velocity, effort, and many other fields can be commanded by using the Command API. A hebi::GroupCommand object is used to store the commands to be sent to the group. We use types from the open-source C++ matrix library Eigen to update many of the fields with vectors of data.

// Create an Eigen vector object filled with zeros
Eigen::VectorXd efforts(group->size());
efforts->setZero();

// Command all modules in a group with this zero force or effort command
hebi::GroupCommand groupCommand(group->size());
groupCommand.setEffort(efforts);
group->sendCommand(groupCommand);
Setting the values in the GroupCommand object does not affect/command any modules until the command is sent to the group with the Group::sendCommand function. Also note that commands can be dropped on particularly poor or congested networks, and the Group::sendCommand function does not provide guaranteed delivery or even an acknowledgement; it is designed for relatively high-frequency applications where commands are resent frequently (e.g. 100Hz). For commands that are only sent once (such as setting gains on the modules), we recommend usage of Group::sendCommandWithAcknowledgement to verify the command was received.
The command affects the actuator control loops only if at least one of the position, velocity, and effort command fields is not cleared. In particular, a command sent with all three of these cleared will not affect the control loop setpoints. However, if only a subset of the fields are set (e.g., only position), then commands from the other fields will be cancelled when this message is receieved. If you want to completely cancel out a previous command (especially when the Command Lifetime is not set!) and turn the control loops off, you can set the commanded value to NAN (see std::numeric_limits::quiet_NaN).
Individual commands only set the low-level controller targets for the current instant in time, and do not calculate a trajectory from the current state to the desired target state. In other words, setting a single position command will result in a step response from the actuator. If you’d like a smooth motion between two or more positions, please take a look at the trajectory API.

The individual hebi::Command elements of a GroupCommand object can also be accessed and updated individually. Note that you cannot create an instance of Command - you only retrieve references through a GroupCommand. The hebi::Command object provides a hierarchical message that provides access to all of the available options that can be sent through the API — for example, updating the name of the module in the following example:

hebi::GroupCommand groupCommand(group->size());
hebi::Command& command = groupCommand[0]; // retrieve reference to command for first module
command.settings().name().set("new name");
bool success = group->sendCommandWithAcknowledgement(groupCommand);

The Command class documentation provides complete documentation of the Command class fields.

Command Lifetime

Commands are only valid for a limited command lifetime, so even unchanging setpoints need to be re-sent before they expire. When set, this means that is a process is killed (e.g., with ctrl-c), commanded motions are aborted. This is set on a per-group basis with the hebi::Group::setCommandLifetimeMs() function. By default, the command lifetime is 100ms. Note that this function will return 'false' if attempting to set a value longer than the maximum allowable command lifetime.

// Sets command timeout to 100 milliseconds
group->setCommandLifetimeMs(100);
// Command must be sent in loop at a faster rate than the lifetime in order to remain in effect.
while (!stop_loop)
{
  group->sendCommand(groupCommand);
  std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
Example Use Cases

The following example shows an open-loop controller commanding sine waves with different frequencies on two actuators using simultaneous position and velocity control.

#include <cmath> // provides M_PI
...
// Two actuators executing sine waves of 1Hz and 2Hz
auto group = lookup.getGroupFromNames({"knee", "ankle"}, {"leg"});
group->setCommandLifetimeMs(20);

hebi::GroupCommand groupCommand(group->size());
Eigen::VectorXd w
  << 2.0 * M_PI,
     4.0 * M_PI;

// It is good practice not to allocate this every loop iteration.
Eigen::VectorXd w_t(2);
double t = 0.0;
double dt = 0.01; // 10 ms
while (!stop_loop) {
  w_t = w * t;
  groupCommand.setPosition(w_t.array().cos());
  groupCommand.setVelocity(w_t.array().sin());
  group->sendCommand(groupCommand);
  std::this_thread::sleep_for(std::chrono::milliseconds(dt * 1000.0));
  t += dt;
}

Note in addition to actuator commands such as position, velocity, and effort, the Command object can be used to set values for other types of modules. Here, we set the value on an I/O board digital output pin to 'high'.

auto group = lookup.getGroupFromNames({"IO_BOARD"}, {"sensor interface"});
hebi::GroupCommand groupCommand(group->size());
groupCommand[0].io().e().setInt(1);
group->sendCommand(groupCommand);

Feedback

A group is responsible for gathering sensor feedback from all contained modules, and synchronizing these into a single Group Feedback structure. By setting the group’s feedback frequency (using Group::setFeedbackFrequencyHz), a background thread will request and synchronize this feedback at a specified rate. The default feedback rate is 100Hz.

The actual feedback frequency is fundamentally limited by the underlying operating system’s scheduler. For example, code that executes at a rate of 1KHz in Linux may be limited to ~640Hz on Windows 7. For typical rates below 250Hz this tends to be irrelevant. The Importance of Metrics and Operating Systems contains more information about the expected performance for various operating systems.
Feedback from multiple modules is typically synchronized to within 1ms. However, the actual timings depends on the network layout and external network traffic. Please see Analyzing the viability of Ethernet and UDP for robot control for more information.

To access this feedback, there are three options:

Get Next Feedback

The first is to call Group::getNextFeedback. This will return the next new (not previously accessed) synchronized feedback. Thus, a single feedback response will never be returned more than once. If there is no new feedback since the last call to this function, it will block and wait for a new packet (up to a configurable timeout), so this can be used as an easy way to control the rate of a loop. For example:

// Best practice is to allocate this once, not every loop iteration
hebi::GroupFeedback feedback(group->size());

// This effectively limits the loop below to 200Hz
group->setFeedbackFrequencyHz(200);

while (!stop_loop)
{
  if (!group->getNextFeedback(&feedback))
    continue;
  // ... read/use feedback object contents here.
}

This use of getNextFeedback parallels the approach taken in the HEBI API for MATLAB.

Feedback Callbacks

The second way to access this feedback is to add a feedback handler that is called each time new synchronized group feedback is available. The C++ 11 std::function will automatically convert function pointers and lambda functions to a function object. The callback will execute on a background thread. Below we show two methods for adding a callback function

// Function feedback callback
static void feedback_func_callback(const hebi::GroupFeedback& const feedback) {
  // ... read/use feedback object contents here
}

void foo() {
  // Sets frequency to 200 Hz
  group->setFeedbackFrequencyHz(200);

  // Method 1: Add pointer to function
  group->addFeedbackHandler(feedback_func_callback);

  // Method 2: Add a lambda function; see C++ reference for more info
  group->addFeedbackHandler(
      [](const hebi::GroupFeedback& const feedback) -> void {
        // ... read/use feedback object contents here
      }
  );
}
Manual Feedback Requests

You can also retrieve feedback when the feedback frequency is zero by manually sending a feedback request. This allows finer grained control over the network traffic that is sent.

hebi::GroupFeedback feedback(group->size());
group->sendFeedbackRequest(); // Sends a request to the modules for feedback and immediately returns
group->getNextFeedback(&feedback); // Blocks until feedback is in the background queue or timeout
The setFeedbackFrequencyHz call effectively calls the sendFeedbackRequest function at a certain rate on a background thread.
Feedback Contents

The GroupFeedback object contains position, velocity, and effort actuator feedback, as well as sensor feedback from a number of sensors. Similar to commands, many of the values can be accessed as Eigen vectors or matrices, with one row for each module in the group. Fields can also be accessed via individual hebi::Feedback object references, which allow hierarchical access to all information contained in the feedback message.

// Fill in feedback
hebi::GroupFeedback group_feedback(group->size());
group->getNextFeedback(&group_feedback);

// Retrieve positions:
Eigen::VectorXd positions = group_feedback.getPosition();
std::cout << "Position feedback: " << std::endl << positions << std::endl;

// Update an existing Eigen vector or matrix (eliminates a copy operation)
Eigen::MatrixX3d gyros(group->size());
group_feedback.getGyro(gyros);
std::cout << "Gyro feedback: " << std::endl << gyros << std::endl;

// Individually access io pin feedback from first module
auto& io_a = group_feedback[0].io().a();
if (io_a.has(1))
  std::cout << "Pin A1: " << io_a.get(1) << std::endl;
Use Case Example

The below example shows a closed-loop controller that implements a virtual spring that controls torque to drive the output towards the origin (stiffness = 1 Nm / rad).

// Virtual spring
hebi::Lookup lookup;
auto group = lookup.getGroupFromNames({"name"}, {"family"}); // change to your name(s)/family(s)
hebi::GroupCommand command(group->size());
hebi::GroupFeedback feedback(group->size());

const double stiffness = 1.0; // [Nm/rad]

while (!stop_loop) {
    group->getNextFeedback(&feedback); // wait for the feedback
    command.setEffort(-stiffness * feedback.getPosition());
    group->sendCommand(command);
}

Using a lambda callback, this can be rewritten as:

// Virtual spring
hebi::Lookup lookup;
auto group = lookup.getGroupFromNames({"name"}, {"family"}); // change to your name(s)/family(s)
hebi::GroupCommand command(group->size());

const double stiffness = 1.0; // [Nm/rad]

while (!stop_loop) {
  // Add callback, capturing relevant variables by reference and/or value
  [&group, &command, stiffness](const hebi::GroupFeedback& const feedback) -> void {
    command.setEffort(-stiffness * feedback.getPosition());
    group->sendCommand(command);
  }
}

Best Practices

Performance and Efficiency

The API has been developed with low latency and high efficiency in mind. For applications that require high control frequencies, we recommend the following additional steps.

  • Reuse Command, Feedback and Eigen objects rather than allocating one each time in a loop

// allocate groupCommand outside tight loop below
hebi::GroupCommand groupCommand(group->size());
hebi::GroupFeedback group_feedback(group->size());
Eigen::VectorXd posVector(group->size());
posVector.setZero();

group->setFeedbackFrequencyHz(100); // set maximum loop speed

while(!stop_loop) {
    // wait for the feedback
    if (!group->getNextFeedback(&group_feedback))
      continue;
    // edit "posVector"
    ...
    // set command data
    groupCommand.setPosition(posVector);
    group->sendCommand(groupCommand);
    std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
  • If using vectorized access to group feedback fields, pass in an existing Eigen object rather than returning a new one

hebi::GroupFeedback group_feedback(group->size());
Eigen::VectorXd positions(group->size());
if (group->getNextFeedback(&group_feedback)) {
  // Prefer passing in reference rather than returning copy
  group_feedback.getPosition(positions);
}

Gains

Control loop gains can be retrieved and set on each device by features provided by the Command and Info APIs.

The recommended way to set and load gains is to use the XML gain format that works across HEBI APIs and GUI tools. To save gains from or load gains onto a module, call the writeGains and readGains functions on the GroupInfo and GroupCommand objects as follows:

// Saving gains from a group of modules:
GroupInfo group_info(group->size());
if (group->requestInfo(&group_info))
  group_info.writeGains("saved_gains.xml");

// Loading gains from a file onto a group of modules:
GroupCommand group_cmd(group->size());
group_cmd.readGains("saved_gains.xml");
bool success = group->sendCommandWithAcknowledgement(group_cmd);

Alternatively, individual gains can be accessed and set programmatically. Upon inspection of the C++ API class hierarchy, you will see that there are nested classes within both the hebi::Command and hebi::Info classes. The example below demonstrates programmatic doubling of the position P gain for each module in the group:

// Double position P gain
hebi::GroupInfo groupInfo(group->size());
hebi::GroupCommand groupCommand(group->size());
group->requestInfo(&groupInfo);

for (int i = 0; i < group->size(); i++) {
    auto& info_kp =
        groupInfo[i].settings().actuator().positionGains().positionKp();
    auto& cmd_kp =
        groupCommand[i].settings().actuator().positionGains().positionKp();
    if (info_kp) {
        cmd_kp.set(info_kp.get() * 2.0);
    }
}
group->sendCommand(groupCommand);

Robot Model / Kinematics

The hebi::RobotModel namespace contains classes and functions to store and work with robot kinematic configurations. These assist with computing forward and inverse kinematics and Jacobians.

Defining the Robot Configuration

The hebi::robot_model::RobotModel class is used to store how different rigid bodies and joints are attached together. There are helper functions to add standard HEBI components to the robot model.

The first body added to the hebi::robot_model::RobotModel object represents the base and the last body represents the end (often an end effector) of the kinematic chain.

Currently the API assumes a serial kinematic chain of bodies. Support for kinematic trees is planned in a future release.

Various factory methods are used to add HEBI components to the robot model; these fall into 3 basic categories: Actuator, Link, and Bracket:

using ActuatorType = robot_model::RobotModel::ActuatorType;
using BracketType = robot_model::RobotModel::BracketType;
using LinkType = robot_model::RobotModel::LinkType;

// Create a simple kinematic description of a 3-DOF robot arm
hebi::robot_model::RobotModel model;
model.addActuator(ActuatorType::X5_4);
model.addBracket(BracketType::X5LightRight);
model.addActuator(ActuatorType::X5_4);
model.addLink(LinkType::X5, 0.18, M_PI);
model.addActuator(ActuatorType::X5_4);
model.addLink(LinkType::X5, 0.28, 0);

The complete list of these methods is documented here. These methods and their named parameters correspond with the actuator, link, and bracket types given in the Kinematic Body Types section.

In addition to standard HEBI components, you can use the addRigidBody and addJoint functions to add any desired masses and degrees of freedom to your system.

Forward Kinematics

The RobotModel::getForwardKinematics (or aliased RobotModel::getFK) method computes the poses of each body in the RobotModel object, using specified values for the joint parameters. Using the HebiFrameType parameter, one can choose either the center of mass or the output frame for each kinematic body.

The resulting 4x4 homogeneous transforms are specified in the world frame of the RobotModel object. This world frame defaults to the input frame of the base module, although a transformation can be prepended using the setBaseFrame function. Units of translation are in meters.

A Matrix4dVector object is passed in to the getFK function, and is resized as necessary and filled in with the computed transforms (in order of the bodies in the RobotModel object). This type is simply a std::vector containing Eigen::Matrix4d elements; due to memory alignment considerations Eigen types in std containers must use particular allocators, and so we provide this convenience typedef. (See https://eigen.tuxfamily.org/dox-devel/group__TopicStlContainers.html for more information on this requirement)

// First, build up a robot_model::RobotModel object, as in previous examples.
...
// These are the current joint angles/positions, perhaps from a GroupFeedback object:
Eigen::VectorXd angles = group_feedback.getPosition();
Matrix4dVector transforms;
model.getFK(HebiFrameTypeOutput, angles, transforms);
for (int i = 0; i < transforms.size(); $$++i$$)
{
  std::cout << "Transform for module " << i << " is " << std::endl
            << transforms[i] << std::endl << std::endl;
}

The RobotModel::getEndEffector method is a shortcut for just obtaining the output transform of the last frame in the RobotModel object:

// Set up hebi::RobotModel object "model" and Eigen::VectorXd "angles" as above...
...
Eigen::Matrix4d transform;
model.getEndEffector(angles, transform);
std::cout << "4x4 transform from base to end effector: " << std::endl
          << transforms[i] << std::endl;

Full API documentation of these functions is available here.

Jacobians

We can also use the RobotModel object to obtain the Jacobian, or the partial derivatives of the forward kinematics equation. This relates the joint velocities to the linear and angular velocity of each body in the system.

The Jacobian in returned as a set of matrices, one for each body in the RobotModel object. Each matrix is of size 6 x numDoF. Rows 1 - 3 of this matrix corresponds to this body’s linear velocities [m/s] along the X-Y-Z axes in the world frame, while rows 4 - 6 correspond to rotational velocities [rad/s] about the X-Y-Z axes in the world frame. The columns define the contributions to these linear and rotational velocities that would be a result of the respective degree of freedom instantaneously moving at unit velocity (e.g., a rotational joint being driven at 1 rad/s).

As with the forward kinematics, we provide versions for the end effector (which just returns a since 6 x numDoF matrix) as well as the entire system. Note that the matrices are now of type Eigen::MatrixXd and MatrixXdVector.

// Set up hebi::RobotModel object "model" and Eigen::VectorXd "angles" as above...
...

// Get Jacobian for all bodies:
MatrixXdVector transforms;
model.getJ(HebiFrameTypeOutput, angles, transforms);

// Get Jacobian for end effector:
Eigen::MatrixXd transform;
kin.getJEndEffector(angles, transform);

Full API documentation of these functions is available here.

Inverse Kinematics

The RobotModel object also provides a method for computing joint angles that solve the inverse kinematics, according to a number of objectives and constraints. Note that the implementation of this function relies on a local optimization, and so is highly dependent on the initial joint angles that are provided as the seed for the search.

The desired objectives and constraints should be passed to the solveIK function as a series of arguments; any number are permitted, but there are limitations in which can be used together. In future API releases, the IKResult return structure will contain more information about the optimization result.

Here is an example of using the solveIK function to find joint angles that move the end effector to a given position:

// Set up hebi::RobotModel object "model" and Eigen::VectorXd "angles" as above...
...

Eigen::Vector3d target_xyz;
target_xyz << 0.4, 0.0, 0.2;
Eigen::VectorXd initial_joint_angles(model->getDoFCount());
Eigen::VectorXd ik_result_joint_angles(model->getDoFCount());

// Fill in 'initial joint angles', perhaps with feedback from the robot.
...

////////////////////////////////////////////////////////////////////////////
// Demonstration of using a single objective:
////////////////////////////////////////////////////////////////////////////

model.solveIK(
  initial_joint_angles,
  ik_result_joint_angles,
  kinematics::EndEffectorPositionObjective(target_xyz)
);

////////////////////////////////////////////////////////////////////////////
// Demonstration of using a custom objective:
////////////////////////////////////////////////////////////////////////////

model.solveIK(
  initial_joint_angles,
  ik_result_joint_angles,
  robot_model::CustomObjective<1>(
    [](const std::vector<double> positions, std::array<double, 1>& errors)
    {
      // Add up all the joint angles
      double sum = 0;
      for (auto p : positions)
        sum += p;
      // This objective prefers joint angles that sum to '2'
      errors[0] = 2 - sum;
    }
  )
);

////////////////////////////////////////////////////////////////////////////
// Demonstration of using multiple objectives (allowed through variadic templates)
////////////////////////////////////////////////////////////////////////////

// Set joint limit constraints:
Eigen::VectorXd min_positions(model->getDoFCount());
min_positions << -M_PI, -1.0f, -1.0f;
Eigen::VectorXd max_positions(model->getDoFCount());
max_positions << M_PI, 1.0f, 1.0f;

model.solveIK(
  initial_joint_angles,
  ik_result_joint_angles,
  kinematics::EndEffectorPositionObjective(target_xyz),
  kinematics::JointLimitConstraint(min_positions, max_positions)
);

Full API documentation of these functions is available here.

Gravity Compensation

Currently, computation of torques for gravity compensation is not supported directly in the API, but our examples repository provides a utility function and examples for adding this into your own system. This is subject to change and inclusion directly in the API in the future. This example demonstrates sending torques to compensate for the effect of gravity for a 3 DOF arm; a simplified version is reproduced below.

#include "util/grav_comp.hpp" // From examples repo

// Set up hebi::Group "group", hebi::RobotModel object "model" and Eigen::VectorXd "angles" as above...
...

// Retrieve masses from robot model for efficient/convenient access later.
Eigen::VectorXd masses(robot_model_->getFrameCount(HebiFrameTypeCenterOfMass));
model.getMasses(masses);

// Define gravity directional vector, relative to the base frame of your robot model
Eigen::Vector3d gravity(0, 0, -1);

Eigen::VectorXd effort;
hebi::GroupFeedback feedback(group->size());
hebi::GroupCommand command(group->size());
while (!stop_loop)
{
  group->getNextFeedback(feedback);
  effort = getGravCompEfforts(
    robot_model,
    masses,
    feedback.getPosition(),
    gravity);
  command.setEffort(effort);
  group->sendCommand(command);
}

Trajectories

A trajectory is a (smooth) motion through two or more waypoints (a waypoint is a specific joint positions for each module in the robot). A trajectory defines the position, velocity, and acceleration for each module at each point in time, from the start to end of the trajectory.

More traditional robot arms usually are controlled by defining waypoints and some motion parameters (e.g., maximum acceleration), as opposed to the joint-level control our API provides at the lowest level. The trajectory API provides a way to define motions in a similar high-level way as these traditional systems, while still allowing access to control and feedback at a lower level.

Generating Trajectories

The hebi::trajectory::Trajectory class is used to generate and access trajectory motions. To create a trajectory object, you must define the waypoints and time at which the robot should pass through each waypoints. Note that the waypoint velocity and acceleration constraints are optional, but it is often useful to at least define the starting and ending velocity and acceleration to be zero. If these are not given, it is left up to the internal implementation to determine values for these.

Below, we demonstate creation of a trajectory for 2 modules over 5 waypoints, using the unconstrained quadratic programming solver:

// Define nan variable for readability below
const double nan = std::numeric_limits<float>::quiet_NaN();

// Position, velocity, and acceleration waypoints.  Each column is a separate
// waypoint; each row is a different joint.
Eigen::MatrixXd positions(num_joints,5);
Eigen::MatrixXd velocities(num_joints,5);
Eigen::MatrixXd accelerations(num_joints,5);
positions << current_position[0], 0, M_PI_2, 0,         0,
             current_position[1], 0, 0,        -M_PI_2, 0;
velocities << 0, nan, nan, nan, 0,
              0, nan, nan, nan, 0;
accelerations << 0, nan, nan, nan, 0,
                 0, nan, nan, nan, 0;

// The times to reach each waypoint (in seconds)
Eigen::VectorXd time(5);
time << 0, 5, 10, 15, 20;

// Define trajectory
auto trajectory = hebi::trajectory::Trajectory::createUnconstrainedQp(time, positions, &velocities, &accelerations);

Executing Trajectories

To play through the trajectory object, we query the position, velocity, and acceleration at each point in time, and send appropriate commands to the robot:

// Follow the trajectory
hebi::GroupCommand cmd(num_joints);
double period = 0.01;
double duration = trajectory->getDuration();
Eigen::VectorXd pos_cmd(num_joints);
Eigen::VectorXd vel_cmd(num_joints);
for (double t = 0; t < duration; t += period)
{
  // Pass "nullptr" in to ignore a term.
  trajectory->getState(t, &pos_cmd, &vel_cmd, nullptr);
  cmd.setPosition(pos_cmd);
  cmd.setVelocity(vel_cmd);
  group->sendCommand(cmd);
  std::this_thread::sleep_for(std::chrono::milliseconds((long int) (period * 1000)));
}

Note that acceleration can not be (meaningfully) directly sent to the actuator, but with knowledge of the dynamics this can be converted to a torque or force to compensate for this acceleration. For improved control, we recommend adding effort commands that are the summation of torques/forces that compensate for gravity and torques/forces that compensate for this acceleration (an example of this is coming soon).

Logging

Data logging, visualization, and analysis tools are a critical part of robotics development. Our logging capabilities allow logging of data from groups of modules in excess of 1kHz, and over extended periods of time (multiple days). Logging is done on a background thread so there is no impact on the control logic, and logging easy to enable/disable, even in complex existing code.

The logging format is common across all HEBI APIs, so logs saved with one API can be read by any other API.

Here is a simple example of logging data; the logging occurs at the group’s feedback frequency.

// get hebi::Group "group" from lookup
...

// Start logging (can also specify log file name as second parameter)
std::string full_log_path = group->startLog("./test_logs/");

// main control logic here
...

// Stop logging
std::shared_ptr<hebi::LogFile> log_file = group->stopLog();

The log data is stored in a file on disk; the name is returned with the initial "startLog" call, and is also retrievable through the LogFile object returned when stopping the log.

Note that is the "stopLog" call returns an empty shared_ptr object, this indicates that the call failed and the log file was not created; this is usually caused by there being no active log that has been started.

The returned LogFile object (see API reference documentation) allows sequential access to the content in the log file; the below example demonstrates analysing a logfile to find the highest torque experienced by each joint:

size_t num_modules = log_file->size();
GroupFeedback feedback(num_modules);
Eigen::VectorXd max_efforts(num_modules);
// If we are trying to find the max, start with the lowest possible number.
max_efforts::setConstant(-std::numeric_limits<double>::infinity());

// Get the component-wise max each feedback entry compared to the running max efforts
while (log_file->getNextFeedback(feedback))
  max_efforts = max_efforts.cwiseMax(feedback.getPositions());

Note that you can also manually create a LogFile object from an existing file (from the MATLAB or C++ API) using:

std::shared_ptr<hebi::LogFile> log_file = hebi::LogFile::open("/path/to/file.hebilog");

C# API

The C# API currently is in development. Documentation will be coming soon.

XML Gains Format

In addition to direct programmatic access to setting controller gains, gains can also be stored in a XML file format that is universal to the HEBI APIs and Scope. This format allows gains to be loaded from a file and sent to a group of modules, or saved from a group of modules to a file.

Saving the gains from a module or group of modules will capture all of the settable parameters, including the control strategy.

The format is series of XML elements whose content is a list of values for each module in the group. For example, this snippet shows position PID gains for a set of 3 modules:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<group_gains>
  <control_strategy>3 3 3</control_strategy>
  <position>
    <kp>1 1.2 3</kp>
    <ki>0 0 1.1</ki>
    <kd>0 0.0001 0.001</kd>
  </position>
</group_gains>

This format supports all available gains for the position, velocity, and effort controllers, as can be seen in our downloadable default gains.

Saving and Loading gains

Scope, as well as the C, C++, Python, and MATLAB APIs all have tools to read and write this gain format.

Note that in each case below the gains will be set on the module, but not "persisted" to internal memory so that they apply on the next reboot of the module. Remember to 'persist' these gains if desired!

In the Scope GUI, the currently active gains can be saved to a file by using the "Save" button (1) on the "Gains" tab, and the gains in a file can be written to a module by using the "Load" button (2). The "Load Defaults" button (3) loads the default gains for the selected module and control strategy.

tab gains buttons

In the MATLAB API, these gains can be saved and loaded using function in the HebiUtils class:

% Saving gains from a group of modules
gains = group.getGains()
gainFile = HebiUtils.saveGains(gains, 'MyGains');

% Loading XML file gains and setting on group of modules
gains = HebiUtils.loadGains(gainFile);
group.send('gains', gains);

In the C++ API, these gains can be saved and loaded by using the following code, assuming you have created a Group (see C++ documentation for more info).

// Saving gains from a group of modules:
GroupInfo group_info(group->size());
if (group->requestInfo(&group_info))
  group_info.writeGains("saved_gains.xml");

// Loading gains from a file onto a group of modules:
GroupCommand group_cmd(group->size());
group_cmd.readGains("saved_gains.xml");
bool success = group->sendCommandWithAcknowledgement(group_cmd);

HEBI Robot Description Format

The HEBI Robot Description Format (HRDF) is an XML-based format that allows kinematics and dynamics information about a robot configuration to be stored as a file that can be used by a variety of tools.

The complete format, including past versions and example files using this format, is documented at http://github.com/HebiRobotics/hebi-kit-xml/.

Briefly, the format consists of a series of XML elements which define a kinematic chain of components. For example, this snippet shows the HRDF for a 5-DOF arm kit:

<robot>
  <actuator type="X8-9"/>
  <bracket type="X5HeavyRightOutside"/>
  <actuator type="X8-16"/>
  <link type="X5" extension="0.325" twist="3.141593"/>
  <actuator type="X8-9"/>
  <link type="X5" extension="0.325" twist="3.141593"/>
  <actuator type="X5-1"/>
  <bracket type="X5LightRight"/>
  <actuator type="X5-1"/>
</robot>

This format supports standard HEBI components, as well as generic rigid bodies and massless joints. As new components are added or new features supported by the URDF format, the version number of the format is incremented increased. Each API version explicitly supports a given version of the format.

Version 1.0.0 of the format supports the following elements. Note the attributes correspond to the documented parameters of the HEBI components.

Element Attribute Description Values

actuator

type (required)

The type of HEBI actuator module

X5-1, X5-4, X5-9, X8-3, X8-9, or X8-16

link

type (required)

The type of HEBI link

X5

extension (required)

The length of the link, from center point to center point of mounting brackets (in meters)

Floating point value

twist (required)

The rotation about the extension axis (tube) of the link (in radians)

Floating point value

bracket

type (required)

The type of HEBI bracket module

X5LightLeft, X5LightRight, X5HeavyLeftInside, X5HeavyLeftOutside, X5HeavyRightInside, or X5HeavyRightOutside

rigid-body

mass (required)

The mass of the body (in kilograms)

Floating point value

com_rot

The rotation matrix describing the orientation of the center of mass relative to the input. This is used for simplifying the inertia tensor description if desired.

space-delimited, row-major 3x3 matrix of floating point values,

com_trans

The x-y-z position of the center of mass relative to the input (defaults to [0,0,0])

Space-delimited, 3-element vector of floating point values

output_rot

The rotation matrix describing the orientation of the output frame relative to the input frame, expressed in the input frame. (Defaults to 3x3 identity matrix)

Space-delimited, row-major 3x3 matrix of floating point values

output_trans

The x-y-z position of the output frame relative to the input, expressed in the input frame. (defaults to [0,0,0])

Space-delimited 3-element vector of floating point values

ixx iyy izz ixy ixz iyz

The moments of inertia, expressed in the center-of-mass frame. (each inertia moment defaults to 0)

Floating point value

joint

axis (required)

The rotational or translational degree of freedom about a principal coordinate axis.

rx, ry, rz, tx, ty, tz