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 a released API that is provided as source code wrapping a C library to guarantee maximum portability across operating systems. It is available at https://pypi.python.org/pypi/hebi-py.

Windows (Win32 / Win64)
Linux (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 (x86-64 / armhf / aarch64)
macOS (64-bit)

ROS

We provide multiple ways of interfacing with HEBI components using ROS. More documentation is available on the ROS wiki at http://wiki.ros.org/hebi_cpp_api_examples.

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 (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 256


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 Scope at all stages of your development, partiularly its plotting and logging capabilities.

Before writing a new program Scope is useful 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 and tuning gains on the live system.

Getting Started


tab welcome


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 Tabs for the various panels:

    • Dashboard shows general device information such as hardware revisions and Ethernet settings, as well as various states.

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

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

    • Firmware Updates allows to safely update device firmware.

  • 3 A link to the main documentation page

  • 4 An overview of available keyboard shortcuts

  • 5 A toggle to enable or disable the device lookup. Disabling will reduce network traffic, but it will keep the individual GUI elements from being updated.

  • 6 A toggle enable or disable the display of additional hardware type and serial number information next to each device in the Device Panel.

  • 7 A button that brings up a dialog with additional settings.


settings


  • 1 A slider for controlling the rate of device search (background lookup) requests. This also represents the rate at which the GUI feedback elements will be updated. Typically, this value should be between 5 - 10 Hz.

  • 2 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 clicking the Refresh button.

  • 3 A toggle to disable hardware acceleration. This may be required on certain versions of Linux (such as Ubuntu 16.04. without the latest updates) to prevent a memory leak in the native Mesa 3D drivers. The changes will be applied on the next start of the application.

Device Panel

The Device Panel shows all devices that are visible, or were recently visible, on the network.

device panel crop

The main action in the panel is to select one or more devices for interaction. When multi-selecting, the darker highlighting represents the device that is currently in focus and corresponds to the feedback shown in the tab 1.

The colored dots indicate the current status:

  • Blue: device is reachable and in Bootloader mode

  • Green: device is reachable and in Application mode

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

You can remove all devices that are not currently on the network (ones represented by a grey dot) by clicking the Clear button 2.

The display can be toggled to show the corresponding hardware types and serial numbers in addition to the settable human readable names 5.

If you have devices that you think are currently on the network, but are not shown in the device panel, try the following:

  • Confirm that the Device Search toggle is enabled 3.

  • Update the network interfaces by clicking the Refresh button in the Settings Dialog 4.

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

  • Confirm that the computer that Scope is running on is on the same network as the devices, and that the computer’s networks settings are properly configured.

Dashboard Tab

The Dashboard Tab shows general device information such as hardware revisions and Ethernet settings.


tab dashboard


  • 1 The name and family of the currently focused device. Names can be changed by pressing the enter key.

  • 2 Switches the focus to the next or previous device within the selected devices.

  • 3 Darker highlight that represents the focused module.

  • 4 Action buttons for commonly useful options such as setting the LED color to help determine the device’s location on a robot.

  • 5 The Status section provides details of the various states that the device is in.

  • 6 The Device section shows the details of the mechanical electrical components of the module as well as information about the currently running firmware. The mechanical details are permanent and will never change.

  • 7 The Connectivity section 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.

  • 8 Toggles between static address and automatic (DHCP) mode. In static mode, the IP address will stay constant and the device will boot up faster, but if the network changes, the device may become unreachable.

  • 9 Shows additional parameters that provide access to hardware features such as zeroing sensors


tab dashboard full


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

  • 2 The Effort Reference 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)

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 A slider to control Frequency that feedback is requested and plotted from a module. This setting effects all open plots.

  • 2 A button to create a .hebilog file of the feedback of all selected modules. The frequency will match the plotting frequency. Only one log can be active at a time.

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

  • 4 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.

  • 5 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.

  • 6 A checkbox and slider to send Commands to the selected modules. The initial commands after enabling the slider will be set to the device’s current state.

  • 7 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 sent from Scope to a module will stay active until another command is sent, and new commands from any API will immediately override the last command from Scope.

If you close Scope, previous commands sent from Scope 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.

Scope is a completely standalone process with a separate network connection that is independent from any user code, and can be run in parallel with any other application on the same computer or local network. When debugging your applications we highly recommend making use of Scope’s plotting capabilities 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 of position, velocity or effort.

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'.

If you want to get an idea of the performance of the underlying network 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 copying files over 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

Custom Charts

It is also possible to create custom plots of live feedback data. This is done by creating a charts directory with .XML files. The directory needs to be located in the same folder as the .jar file for Scope:

  • For the Windows and Linux this is: Scope/app/charts/

  • For macOS, right-click the Scope.app icon and select "Show Package Contents". Then navigate to Contents/Java/charts/

The .XML file needs to have 2 elements, one <chart> and at least one <trace>.

Valid .XML files are loaded in alphabetical order and added as separate buttons in the Custom section of the Monitoring tab. The label that appears on the button will be whatever is put for the title attribute in <chart>.

<chart>

  • Attributes:

    • <title>: Title for the button and plot header

    • <min_range>: Number, defaults to 0

    • <max_range>: Number, defaults to 0

    • <range_policy>: Behavior if trace values go out of range. Can be fixed or expanding (default).

  • Children:

    • <trace>: One or more. Each trace will be shown as a separate line.

<trace>

  • Attributes:

    • <label>: Name of this trace

    • <units>: Units

    • <color>: red, green, blue,yellow, cyan, magenta, white, black (default),

    • <style>: solid (default), dashed, points

    • <value>: A custom expression for calculating the current value. You have access to the latest feedback values (fbk) as well as the previous values (prevFbk). The field naming convention matches MATLAB, e.g., fbk.position or prevFbk.hwTxTime.

Examples

In custom charts you can use operators like + - * / to do basic math on different parts of module feedback. The example below creates a plot that calculates the position error, which can often be useful when tuning or evaluating the performance of a system:

<chart title="POSITION ERROR" min_range="-0.5" max_range="+0.5">
    <trace label="Position Error" units="rad" color="black" style="dashed" value="fbk.position - fbk.positionCmd" />
</chart>

The expression parser uses a JavaScript compiler, so you may make use of math functions such as pow or sqrt. The example below calculates the magnitude of all three components of the accelerometer of a module.

<chart title="ACCEL MAGNITUDE" range_policy="fixed" min_range="0" max_range="20">
    <trace label="sqrt(x^2 + y^2 + z^2)" units="" color="black" style="solid"
           value="Math.sqrt(Math.pow(fbk.accelX,2) + Math.pow(fbk.accelY,2) + Math.pow(fbk.accelZ,2))" />
</chart>

Additional lines can be added with addititional <trace> children. You can also access the previous feedback to perform diffs over a single timestep. The example below numerically differentiates the commanded position and compares it to the commanded velocity.

<chart title="DIFF(POSITION CMD)" min_range="-3" max_range="+3" range_policy="expanding">
    <trace label="diff(positionCmd)" units="rad/s" color="black" style="points" value="(fbk.positionCmd-prevFbk.positionCmd) / (fbk.hwTxTime-prevFbk.hwTxTime)" />
	<trace label="velocityCmd" units="rad/s" color="green" style="dashed" value="fbk.velocityCmd" />
</chart>


tab monitoring custom plot


Gains Tab

The Gains Tab provides a convenient way to change control strategies 7, set safety limits 10, and to set gains 11. Tips and guidelines on tuning gains can be found in the Motion Control documetation.


tab gains


Gains can be set while a device is actively being commanded, either by Scope or from any of the APIs, by clicking Send 2 at any time. This allows easy online tuning of a robot’s motion. For example, you can generate commanded positions, velocities, 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.

If you are commanding from Scope you will need to make sure you are not also setting the control strategy (the dropdown box is blank). In this case the command will be cleared when sending gains.

Sending gains will send all the fields shown. Empty fields will be ignored. You can clear the fields by clickin the Clear button 3.

The default view of Gains show some of the most commonly adjusted parameters for the position, velocity, effort controllers on an actuator. The full list of parameters can be viewed by toggling the Show Advanced switch 9. Details on all of these parameters can be found in the Motion Control documentation.

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 enable the "Persist Sends" toggle 8 or click persist on the Dashboard Tab.

Saving/Loading Gain files

Gains can be saved in a group-compatible gain format by selecting one or more devices and pressing the save to file button 4. The gains of multiple devices will be stored in the same order as they appear on the device pane.

Load file 5 reads a gain file and loads the values into the corresponding input fields. Only one set of gains can be displayed at a single time.

Send File 6 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 1, 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


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 Refresh button 2. 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.


tab update check


The refresh will show all modules for which updates are available 1. You can then update individual devices, or all of them at once 3. The devices will automatically reboot into Bootloader, download the appropriate application firmware, and reboot.


tab update program


Updating a device 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.


VPN Gateway

It is sometimes desired to connect to devices on a remote network. Unfortunately, most VPN solutions either block broadcasts and/or don’t allow connections outside the host running the VPN.

To help with this problem, we developed an experimental Gateway that can do a lookup on a local network and expose the found devices via a single address to remote clients. Hence with this feature, if computer 1 is running the gateway, computer 2 can connect to and interact with the modules located on computer 1’s network as if they were local to computer 2.

While this tool is not directly related to Scope, we bundled it inside the Scope (>=1.6.2) application for convenience. You can execute it from commandline as follows:

  • Linux

cd Scope/lib
./runtime/bin/java -cp app/hebi-scope-*.jar us.hebi.gui.tools.UdpGateway
  • Windows

cd Scope
.\runtime\bin\java -cp app/* us.hebi.gui.tools.UdpGateway

When running the app, there is a print out with a Server is locally reachable at section that lists all the available addresses of computer 1. Computer 2 needs to pick an appropriate one from the list and then add it to the lookup addresses in Computer 2’s Scope or the APIs.

In case you are not familiar with setting up VPNs, we recommend taking a look at ZeroTier. It can setup a private network with bi-directional communications between multiple individual computers across the Internet.


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

In both Android and recent iOS versions, the name and family of the mobile device can be set using Scope by right-clicking the mobile device on the left side panel, or by setting the name and family from any of the APIs.

Android

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. Finally, you can select whether or not the acceleration feedback that is returned from the application includes the acceleration due to gravity, or filters this information out.

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.

Device Commands (iOS only)

The Mobile I/O applications also can be configured by and display/react to commands sent from Scope or the APIs.

I/O Commands

Parameter Units Description

a1 - a8

-1 to 1

Sets the "snap to" location for a joystick axis (default 0) or slider (default disabled). Sending nan disables the snap behavior.

b1 - b8

0 or 1

Sets the button behavior to momentary (default, 0) or toggle (1). The toggle state is identified by white text on the button, whereas the momentary state is black.

e1 - e8

0 or 1

Illuminates (1) or hides (0) a indicator ring around the corresponding button.

f1 - f8

-1 to 1

If a joystick axis or slider is not in "snap" mode, and is not actively being moved, then this moves the joystick axis or slider to the given location. For joystick commands that are out of the unit circle range, this projects the desired point back to the unit circle.

General Commands

Parameter Units Description

led

RGB(A)

Sets the color of a border around the outside perimeter of the app

effort

n/a

For non-zero commanded values, causes the app to vibrate momentarily (on supported devices)



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. For more details, please refer to the Simulink Support thread on the forums.

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.

If you do not have access to physical devices, you can still test the API or control algorithms using group = HebiUtils.newImitationGroup(numDevices).

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();

% Mobile device feedback including location services and AR features
mobileFbk = group.getNextFeedbackMobile();

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 safety 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);
GainStruct

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.

% Send guaranteed and persist permanently (don't call this in a control loop)
while ~group.send('gains', gains, 'RequestAck', true, 'Persist', true,);
end

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);

Gain structs can also be saved and loaded from disk using saveGains and loadGains. The files use the XML Gains Format and can be edited using a standard text editor.

% Save gains to disk
gains = group.getGains();
HebiUtils.saveGains(gains, 'myGains.xml');

% Load gains from disk and send with guaranteed delivery
gains = HebiUtils.loadGains('myGains.xml');
while ~group.send('gains', gains, 'RequestAck', true);
end

Safety Parameters

The SafetyParamsStruct can be used to change the safety parameters executed on each device.

% Limit position to +/- pi
safetyParams = group.getSafetyParams();
safetyParams.positionLimitStrategy = 3; % damped spring
safetyParams.positionMinLimit = -pi;
safetyParams.positionMaxLimit = +pi;
group.send('SafetyParams', safetyParams);
SafetyParamsStruct
For information regarding strategy values see SafetyParamsStruct.

Safety parameters can be saved and loaded from disk using saveSafetyParams and loadSafetyParams. The files use the XML Safety Params Format and can be edited using a standard text editor.

% Save safety parameters to disk
safetyParams = group.getSafetyParams();
HebiUtils.saveSafetyParams(safetyParams, 'mySafetyParams.xml');

% Load safety params from disk and send with guaranteed delivery
safetyParams = HebiUtils.loadSafetyParams('mySafetyParams.xml');
while ~group.send('SafetyParams', safetyParams, 'RequestAck', true);
end

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.

Robot configurations can be loaded from XML files using the HEBI Robot Description Format (HRDF) or programmatically using the API.

% Loading a robot description (hrdf) from file
kin = HebiKinematics('MyRobotDescription');

Programmatically 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
<?xml version="1.0" encoding="UTF-8"?>
<robot version="1.1.0">
  <actuator type="X5-4"/>
  <link type="X5" extension="0.216 + 0.0715" twist="0" mass="0.318"/>
  <actuator type="X5-1"/>
  <link type="X5" extension="0.332 + 0.0715/2" twist="0" mass="0.276"/>
</robot>
X-Series SCARA-ish Kit (3-DoF)
kin 3dof x series SCARA ish
The configuration of kits may change over time. The below code describes what is shown in the image, but it may not match the latest versions of this kit. Please refer to the Github examples for the most recent kit configurations.
% X-Series SCARA-ish Kit (3-DoF) (pre X8 version)
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 );
<?xml version="1.0" encoding="UTF-8"?>
<robot version="1.1.0">
  <actuator type="X5-4"/>
  <bracket type="X5LightRight"/>
  <actuator type="X5-9"/>
  <link type="X5" extension="0.131 + 0.0715" twist="pi/2" mass="0.15"/>
  <actuator type="X5-4"/>
  <link type="X5" extension="0.109 + 0.0715" twist="0" mass="0.15"/>

   <!-- Screwdriver End Effector (X5-1 actuator) -->
   <rigid-body com_trans="0 0 0.015" output_trans="0 0 0.058" mass="0.35"/>

</robot>
X-Series Manipulator Kit (5-DoF)
kin 5dof x series manipulator
The configuration of kits may change over time. The below code describes what is shown in the image, but it may not match the latest versions of this kit. Please refer to the Github examples for the most recent kit configurations.
% 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 );
<?xml version="1.0" encoding="UTF-8"?>
<robot version="1.1.0">

  <!-- 5-DoF Arm -->
  <actuator type="X5-4"/>
  <bracket type="X5HeavyRightOutside"/>
  <actuator type="X5-9"/>
  <link type="X5" extension="0.379" twist="pi"/>
  <actuator type="X5-9"/>
  <link type="X5" extension="0.252" twist="pi"/>
  <actuator type="X5-1"/>
  <bracket type="X5LightRight"/>
  <actuator type="X5-1"/>

  <!-- Generic Gripper -->
  <rigid-body com_trans="0 0 0.0125" output_trans="0 0 0.025" mass="0.1"/>

</robot>

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 orientation values of a module. The orientation Quaternion is determined by locally fusing the accelerometers and gyroscopes and may be used while the robot is moving.

% automatically determine direction of gravity based on the local pose filter
fbk = group.getNextFeedbackFull();
q = [fbk.orientationW(1) fbk.orientationX(1) fbk.orientationY(1) fbk.orientationZ(1)]
baseRotMat = HebiUtils.quat2rotMat(q);
gravityVec = -baseRotMat(3, 1:3);

% 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 / pose filter.
fbk = group.getNextFeedbackFull();
q = [fbk.orientationW(1) fbk.orientationX(1) fbk.orientationY(1) fbk.orientationZ(1)]
baseRotMat = HebiUtils.quat2rotMat(q);
gravityVec = -baseRotMat(3, 1:3);

% 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

The API also includes the utility plotTrajectory method.

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

NOTE: As of hebi-py version 2.0.0, only Python 3 is supported. You must use a hebi-py version 1.x.y for Python 2 compatibility.

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 3 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).

It is recommended to install matplotlib through your package managers on most Linux distros. Matplotlib’s website documentation elaborates more on specific dependencies and how to compile from source.

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 uses the package python3-numpy; Red Hat/CentOS/Fedora use 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.

import hebi
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(f'{entry.family} | {entry.name}')

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

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
import hebi

# 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 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:

import hebi
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(["leg"], ["knee", "ankle"])
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'.

import hebi
from time import sleep

lookup = hebi.Lookup()
sleep(2.0)

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 is 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(f'Position Feedback:\n{positions}')

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

# Individually access io pin feedback from first module
io_a = group_feedback.io.a
if (io_a.has(1)):
  print(f'PinA: {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).

import hebi

# Virtual spring
lookup = hebi.Lookup()
# change to your name(s)/family(s)
group = lookup.get_group_from_names(["family"], ["name"])
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:

import hebi

# Virtual spring
lookup = hebi.Lookup()
# change to your name(s)/family(s)
group = lookup.get_group_from_names(["family"], ["name"])
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)

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 is not 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.

Robot configurations can be loaded from XML files using the HEBI Robot Description Format (HRDF) or programmatically using the API. It is strongly recommended that you use the HRDF toolset, and load your robot configuration in your code as follows:

import hebi

# Load the RobotModel for a 6-DoF Arm
arm = hebi.robot_model.import_from_hrdf("6-dof_arm.hrdf")

You can also build your robot component by component in your code by specifying 3 types of components: Actuator, Link, and Bracket. A complete list of these methods is located here.

These methods and their named parameters correspond with kinematic 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 as well.

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(f'Transform for module {i} is\n{transform}\n')

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(f"4x4 transform from base to end effector: {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:

import hebi
import numpy as np

# Populate variable 'current_position' with position feedback

# 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:

import hebi
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 is not 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:

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

Best Practices

Performance and Efficiency

Caveats

The API has been developed with low latency and high efficiency in mind. However, users of this Python API should be aware of the CPython (the defacto Python implementation from python.org) implementation’s inherent deficiencies in performance (without the use of C wrappers, ctyhon, etc).

After finding the bottlenecks in your code, it is recommended to do one of the following:

  • Move performance critical code (e.g., kinematics, balance controllers, impedance controllers, etc ) into C/C++ and invoke from Python using ctypes/https://cffi.readthedocs.io/en/latest/[cffi]

  • Try using PyPy3 instead of CPython as your Python interpreter (Note that this is still not an officially supported Python runtime for our API)

  • Use HEBI’s C++ API when the best performance is require, especially on slower devices such as a Raspberry Pi

Our empirical tests have shown that complicated samples using our API (e.g., our Igor kit in Python) are constrained, computationally, by CPython and not the underlying logic within the Python API.

Recommendations

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

import hebi
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 is 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)

C++ API

Note: this documentation refers to the C++ 3.3.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.

Robot configurations can be loaded from XML files using the HEBI Robot Description Format (HRDF) or programmatically using the API. It is strongly recommended that you use the HRDF toolset, and load your robot configuration in your code as follows:

// Load the RobotModel for a 6-DoF Arm
std::unique_ptr<hebi::robot_model::RobotModel> arm = hebi::robot_model::RobotModel::loadHRDF("6-dof_arm.hrdf");

Make sure your HRDF file is saved at an appropriate location or loadHRDF will return an empty unique_ptr.

You can also build your robot component by component in your code by specifying 3 types of components: Actuator, Link, and Bracket. A complete list of these methods is located here.

These methods and their named parameters correspond with kinematic 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 as well.

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 hebi::robot_model::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();
hebi::robot_model::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");

ROS API

The ROS documentation is primarily maintained at wiki.ros.org, in particular at http://wiki.ros.org/hebi_cpp_api_examples and http://wiki.ros.org/hebi_cpp_api

Overview

To make it convenient to use ROS with HEBI products, we have created a set of nodes and full system examples to allow you to interface with HEBI systems. These examples include using ROS to control a HEBI arm, mobile bases, integration with MoveIt!, Gazebo simulation integration, and Rviz visualization. These are documented on the ROS wiki at http://wiki.ros.org/hebi_cpp_api_ros_examples.

We also provide a library of XACRO macros in the hebi_description package to help you build up systems from HEBI components quickly.

Internally, these examples use the HEBI C++ API to connect to HEBI components. For those wishing for more flexibility or to develop custom nodes, we have created a ROS package that wraps the HEBI C++ API (http://wiki.ros.org/hebi_cpp_api). This can be installed as a ROS package on kinetic or melodic using apt install ros-<distro>-hebi_cpp_api. For use of the C++ API in ROS, the examples shown above provide a good starting point; further documentation is available above.

Other approaches

Python It is also possible to call the HEBI Python API directly in your node; we do not currently have public examples for this approach.

Deprecated ROS API Previously, HEBI created the hebiros package (http://wiki.ros.org/hebiros) which exposed HEBI C++ API functions entirely over ROS messages and actions. This was deprecated due to this approach not scaling to larger systems. If porting old systems dependent on hebiros or looking for a simple use of the new ROS api with individual actuators, we recommend using the group_node from the HEBI C++ examples repository.

The deprecated hebiros package is no longer being actively maintained, and we highly recommend anyone still using this to transition to the hebi_cpp_api and hebi_cpp_api_examples packages. Please email support@hebirobotics.com for assistance in this process!

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 to File" button (1) on the "Gains" tab, and the gains in a file can be written to a module by using the "Load File" button (2). The "Load Defaults" button (3) loads the default gains for the selected module and control strategy. Differences between the input fields and the currently set values will be highlighted.

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);

XML Safety Parameters

In addition to direct programmatic and UI access to setting safety parameters such as position limits, m-stop strategies, etc, these can also be stored in a XML file format that is universal to the HEBI APIs and Scope.

This format allows safety parameter to be loaded from a file and sent to a group of modules, or saved from a group of modules to a file.

Currently, this is only supported by the MATLAB API, but will be added to the C++ and Python APIs in upcoming releases.

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 limits and posiiton limit strategies for a set of 4 modules:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<group_safety version="1.0.0">
  <mstop>
    <strategy>off hold disabled off</strategy>
  </mstop>
  <position>
    <limit_strategy>hold off disabled spring</limit_strategy>
    <min_limit>-1.5 0 -inf -inf</min_limit>
    <max_limit>1.5 1 inf inf</max_limit>
  </position>
</group_safety>

This format is fully documented here.

Saving and Loading gains

The MATLAB API is currently the only API supporting reading and writing this format.

Note that in each case below the parameters 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 MATLAB API, these params can be saved and loaded using function in the HebiUtils class:

% Example (Saving gains to XML file)
safetyParams = group.getSafetyParams();
HebiUtils.saveSafetyParams(safetyParams, 'mySafetyParams.xml');

% Example (Loading gains from XML file and setting on a group):
safetyParams = HebiUtils.loadSafetyParams('mySafetyParams.xml');
group.send('SafetyParams', safetyParams);

HRDF

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-hrdf/.

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

API Best Practices

Naming Conventions

In order to have consistency across systems, apps, apis, and example code HEBI Robotics uses a standard convention for naming hardware that makes use of simple letter and number combinations. All HEBI products follow these conventions when shipped, and we encourage all our users to use it as well.

Family Names

Families help to group modules together. Doing so correctly is especially useful and important when you have multiple robots connected to a network at the same time. Check our documentation on Groups to learn more.

All actuators involved in a given kit (Igor, Hexapod, etc.) should be assigned the same Family. As such, the Family assigned should represent the name of that kit.

For example, all actuators in an X-Series Hexapod kit would be have the family Daisy.

Remember to include all modules involved with the kit. Don’t forget mobileIO and gripperSpool!

By convention, Family names should:

  • Be uppercase (i.e. Rosie)

  • Use your actuator model for single actuators (i.e. X8-16)

  • Use IO_basic for IO boards

Actuator Names

Our actuators look the same but can be used in multiple different ways; using our conventions can save you time and effort in identifying and debugging the right actuator.

Actuators in Serial Configuration

J#_<type>

Here, J# indicates where the actuator exists along the chain (it is the #th actuator in the chain) and <type> describes what the actuator is acting as. For example J2_shoulder would be the second actuator in the serial chain and the shoulder joint in an arm.

Thus, the standard naming for a 6-DoF arm:

J1_base, J2_shoulder, J3_elbow, J4_wrist1, J5_wrist2, J6_wrist3

If there is only one wrist actuator, like in a 4-DoF arm kit, remove the trailing number: J4_wrist

Special Conditions for Arms

  • If you have 2 or more elbows: J3_elbow1, J3_elbow2, etc…​

  • If you have 2 actuators in parallel: J2A_shoulder, J2B_shoulder

    • A is the one the HRDF follows through.

    • The HRDF will typically follow the left side (relative to the base module). Hence, the convention follows as Left = A and Right = B, but ALWAYS double check with the HRDF.

  • The 2 rules above can combine: J2A_shoulder1, J2B_shoulder1, J3_shoulder2, and so on.

Grippers

If your kit makes use of a standard HEBI parallel gripper, you will have a spool actuator that you command to change the state of the gripper. As per our convention, this actuator is named gripperSpool.

Mobile Base Wheels

  • Omni-base: W1, W2, W3 (refer to Omni-Base Reference Drawing)

  • Diff Drive Base: W1_left, W2_right

  • Mecanum Base: W1_frontLeft, W2_backLeft, W3_frontRight, W4_backRight

Multi-limbed Robots

L#_J#_<function>

Here, L# indicates which limb we are looking at. Each standard robot kit created by HEBI Robotics has a predefined layout of limb numbers that can be found on that kit’s reference documentation.

Here is an example of how the "Daisy" Hexapod kit is named (see page 31 of Hexapod Manual for leg layout):

L1_J1_base, L1_J2_shoulder, L1_J3_elbow, L2_J1_base, L2_J2_shoulder, L2_J3_elbow, etc…​

Special Condition: Igor

For the Igor kit the arms and legs have been named in the following fashion:

  • Left = 1 and Right = 2 (Igor Manual)

  • Legs: L1_J3_wheel, L2_J3_wheel, L1_J1_hip, L1_J2_knee, L2_J1_hip, L2_J2_knee

  • Arms: A1_J1_base, A1_J2_shoulder, A1_J3_elbow, A1_J4_wrist, A2_J1_base, A2_J2_shoulder, A2_J3_elbow, A2_J4_wrist

Mobile devices

If you are using your mobile device to provide input to your program, your device should be named mobileIO. This can be set in the settings on your phone, or using Scope.

Single Actuators

Use the actuator’s serial number. For example: X-00542. This number is printed on the actuator, or can be seen on Scope.

IO Boards

Use the board’s serial number. For example: IO_00042. This number is printed on the board, or can be seen on Scope.

Note that IO board serial numbers use underscores instead of hyphens.

Arm Kit Numbering

Our arm kits are numbered to make it faster for you to identify one arm from another. By looking only at an arm kit number, you should be able to discern which type (X-series or R-series) of actuators your arm is made of, the degrees of freedom in you arm, and the addition of any add-ons, such as a gripper, to your configuration. We use this numbering to name our kinematic (.hrdf) and gains (.xml) files.

Grippers are included in the HRDF file, but not the gains file. For example, in the case of a 6-dof x-series arm with a gripper, the HRDF will be named A-2085-06G.hrdf, but the gains file will be A-2085-06.xml.

Learn more about our arm kits at our Kit documentation or our Arms page.

Arm Kit Number Description

A-2085-##

X-Series arm kit with ## degrees of freedom.

A-2085-##G

X-Series arm kit with an X-Series gripper as the end effector.

A-2240-##

R-Series arm kit with ## degrees of freedom.

A-2240-##G

R-Series arm kit with an R-Series gripper as the end effector.

Special Cases
Arm Kit Number Description

A-2084-01

X-Series SCARA style arm with 4 degrees of freedom.

A-2099-07

X-Series double shoulder arm kit with 7 degrees of freedom.

A-2302-01

R-Series SCARA style arm with 4 degrees of freedom.

A-2303-01

R-Series double shoulder arm kit with 7 degrees of freedom.