Apps and APIs

To control and use HEBI hardware we provide Applications and APIs that run on a wide variety of operating systems.

Applications

Applications include GUIs that provide a user-friendly interface for things like viewing feedback, configuring modules, or getting feedback from other devices.

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)


APIs

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.

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

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

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


File Formats

HEBI uses a number of file formats to store robot configuration information in a way that is easy to version and access across the different Apps and APIs. These include kinematic information, joint-level gains, motor driver settings, and high-level robot configuration files.

File Format Description Supports

A config file format designed to store configuration information about a full system in a human-readable cross-API format.

Scope
All APIs
(experimental)

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

Scope
All APIs

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

Scope
Scope CLI
All APIs

A format for saving and loading all the setting safety parameters (e.g. position/velocity/torque limits, m-stop strategies) for individual modules, as well as groups of modules.

Scope
Scope CLI
All APIs

A format for saving and loading all the configuration parameters for HEBI’s configurable motor driver.

Scope
Scope CLI



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, particularly 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 starts with a screen that looks similar to the screenshot above. The items in the screenshot are, respectively:

  • A Welcome screen that provides links to the main documentation page as well as an overview of keyboard shortcuts

  • The Device Panel on the left that shows all devices visible on the network.

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

    • Motor Driver sets parameters for motor driver devices.

    • Firmware Updates allows to safely update device firmware.

  • Configuration options

    • Device Search enables or disables the device lookup. Disabling will reduce network traffic, but it will keep the individual GUI elements from being updated.

    • Sort Names enables or disables the alphabetical sorting of device names. The device order can be manually modified using ALT + UP or ALT + Down

    • Settings: brings up a menu with additional settings as shown in the screenshot below. Please refer to the tooltips for detailed information.


settings


In Settings you can also choose between different coloring themes and perform manual update checks.


settings dark


Device Panel

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

device panel crop device panel crop dark

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 information tabs.

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.

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.

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

  • 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


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

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

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

  • The Advanced slider shows additional parameters that provide access to hardware features such as modifying internal encoder offsets. This should be used with caution. Refer to the tooltips for more information.

The Imitation Devices button launches a set of networked imitation modules that can be used to test and debug code before running it on real hardware. Imitation devices return commands as feedback and do not include physics feedback such as IMU data.


imitation devices


View Stream displays the contents of a video stream file created by hebi-video. It provides a way to view arbitrary video sources with relatively low overhead and latency.


video view


Monitoring Tab

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


tab monitoring


The Monitoring Tab has the following main features:

  • Plotting buttons open live charts of the selected devices at the update frequency specified by the frequency slider. The time-axis is specified by the time range, and the y-axis of charts is scaled automatically. Clicking on the legend toggles the visibility of individual traces.

  • Command sliders enable sending of basic commands to the selected modules. Commands can be any combination of position, velocity, and effort. The initial commands after enabling the slider will be set to the device’s current state. Specific values can be specified using the target buttons.

  • View HRDF creates a 3D visualization of the HRDF contents and maps the contained joints to zero or more selected devices.

  • Start Log creates a .hebilog file of the feedback of all selected modules at specified feedback frequency. Only one log can be active at a time.

  • View Log visualizes the data inside a .hebilog file.

  • Convert Log converts the contents of a .hebilog file to non-proprietary formats such as CSV or MAT.

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.


plot position


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

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.

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.

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 delta 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

HRDF Viewer

View HRDF creates a 3D visualization of the specified .hrdf file. If devices are selected, the devices get mapped to the joints and create an online visualization of the robot configuration.


hrdf viewer 6dof hrdf viewer tready


It also provides additional features such as displaying a command overlay or to orient the robot based on the first device’s IMU data. Please refer to the menu for more details.

Log Viewer

View Log opens a log viewer that shows many useful plots and provides access to the logged gains.


log viewer


Load HRDF lets users choose a matching HRDF file to get additional charts in end-effector coordinates as well as replay the contents in a 3D visualization.


log view hrdf


Custom Charts

It is also possible to create custom plots of feedback data. This is done by placing .XML files in the charts directory. The charts directory can be viewed and/or specified in the Settings menu.

The .XML file needs to have 2 elements, one <chart> and at least one <trace>. Valid 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 matches the title attribute in <chart>.

<chart>

  • Attributes:

    • title: title for the button and plot header

    • units: Y-Axis units

    • max_samples: maximum rolling buffer size

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

    • <axis_x>: one or none. Defaults to a time axis

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

<axis_x>

  • Attributes:

    • label: x axis label

    • value: x values

    • units: x axis units

<trace>

  • Attributes:

    • <label>: Name of this line in the legend

    • <units>: Units

    • <color>:

      • red, green, blue,yellow, cyan, magenta, white, black (default),

      • [0] default color for the specified index

      • matlab[0] color for the specified index matching MATLAB’s default color scheme

      • [x]/[y]/[z] colors used for XYZ charts,

    • <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 Java 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, set safety limits, and to save and load gains. Tips and guidelines on tuning gains can be found in the Motion Control documentation.


tab gains


Gains can be set while a device is actively being commanded, either by Scope or from any of the APIs, by clicking 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 sends all the fields shown. Empty fields are ignored. You can clear the fields by clicking the Clear button.

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. 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 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. The gains of multiple devices will be stored in the same order as they appear on the device pane.

Load file 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 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, 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


Motor Driver Tab

The motor driver tab provides control of parameters related to turning a custom encoder and motor into a HEBI compatible actuator. Please refer to the built-in tooltips for more information on the individual fields.


tab motor driver


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. 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. You can then update individual devices, or all of them at once. 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.


Scope CLI

Scope ships with an additional command line interface (CLI) that provides useful file loading and configuration utilities without requiring the graphical interface. On Windows and Linux the command line tools are installed automatically. On macOS you need to click Add to shell path in the macOS specific settings.


settings macos


hebi-config

The hebi-config tool can send configuration files to specified devices. Calling hebi-config --help prints a detailed list of all available parameters.

For example, sending a motor driver configuration to a named device looks like this

hebi-config --name MD-210 --motor-driver path/to/MD-210.xml --persist --reset

Multiple entries can be added multiple times or in comma separated lists

hebi-config --name J1_base --name J2_shoulder --names J3_elbow,J4_wrist1,J5_wrist2, J6_wrist3 --gains path/to/arm_gains.xml

hebi-gateway-server

It is sometimes desired to connect to devices on a remote network through tunnels or VPNs. Broadcasts are generally limited to the local network, so we developed a Gateway connection that can forward local lookup requests to a single remote address. With this feature, if computer 1 is running the gateway, a secondary computer 2 can connect to and interact with the devices located on computer 1’s network as if they were local to computer 2.

The exposing computer 1 needs to run hebi-gateway-server. The tool prints out a Server is locally reachable at section that lists all the available addresses of computer 1.


gateway start


The connecting computer 2 can connect to computer 1’s network by running hebi-gateway-client with an appropriate lookupAddress from the list, or by manually adding the address into the Scope settings or in supported 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.


WSL2 Gateway

Windows Subsystem for Linux v2 (WSL2) lets Windows users run a GNU/Linux environment that is very tightly integrated with the host OS. While the Linux versions of the HEBI APIs and tools can run without modification, the environment runs inside a virtual machine that is on a separate network and prevents any devices from being seen.

Until WSL2 adds features to expose raw network interfaces, this limitation can be worked around by (1) launching the VPN Gateway on the Windows side,

# run on Windows
hebi-gateway-server

and subsequently (2) adding the shown IP address on the WSL2 Linux side.


gateway scope


Scope with basic plotting capability can be run using X-forwarding. For setup instructions please refer to How to set up working X11 forwarding on WSL2.

When you run code on WSL2 you can expect the frequency to track significantly better (green dots) at the cost of slightly higher latency (blue dots) due to the additional indirection. The charts below show latency plots running simultaneously on the same computer on Windows (left) and Ubuntu 20.04 on WSL2 (right):


windows vs wsl2


hebi-video

The video acquisition tool for Scope’s video stream viewer is packaged as a separate app due to size concerns. You can download the latest release.

Downloading the HEBI Video .exe application from the link above will install all of the files required to run the commands below from your OS command terminal.

Video streams can be acquired from different sources, including Webcams, IP-cameras, or video files. The stream acquisition updates a memory mapped file, which can simultaneously be viewed in Scope and be opened in an API. Streams can be restarted or changed at any time and do not crash the viewer.

Below are some examples for different sources:

# Syntax
openStream {outputFile} {source} ({width} {height} {fps})

# First USB camera on Windows at 1920p resolution
openStream usb0.stream 0 1920

# Streaming an input file
openStream file.stream recordedVideo.mp4

# Stream from an IP-camera using rtsp
openStream ipcam.stream rtsp://${user}:${pw}@${ip}/stream0

The acquisition is configured to prefer low latency over saving computation time or reducing artifacts. openStream is based on bundled versions of FFMpeg and OpenCV, and is using software decoding.

The package also bundles a GStreamer based OpenGStreamerStream with support for hardware decoding for improved latency. Depending on your system you may need to install the GStreamer library (see instructions).

# Stream from an IP-camera using rtsp w/ possible hardware decoding
OpenGStreamerStream ipcam.stream rtsp://${user}:${pw}@${ip}/stream0

Using hebi-video with the HEBI CR1 Camera Module

HEBI Camera Modules use RTSP to stream the camera feed over the network connection. In order to visualize the camera stream, determine the IP Address of the RPi in the Camera Module through Scope as shown below.

cr1 ip

Once you have located the RPi IP Address, use it with the following command:

# Stream from a HEBI RPi Camera Module
openStream CR1.stream rtsp://{ip}:8554/test

# Example with IP shown above
openStream CR1.stream rtsp://10.10.10.247:8554/test

Running this command will generate a .stream file that scope can use to view the stream with the View Stream button.

cr1 viewstream


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

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.

Device Discovery

The first call to HebiLookup initializes a background process that can automatically discover devices 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 devices 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 devices, you can use the group interface.

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

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

% reset everything to the initial state
HebiLookup.initialize()

% rebuild only the device table
HebiLookup.clearModuleList();
pause(0.5); % allow time to re-build

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

Problem Suggestions

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

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

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

  • Check that the device 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 devices
HebiLookup.initialize();
disp(HebiLookup);

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

Joint-Level Control

Device Selection

Devices 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 devices, and handle high-level issues such as data synchronization and logging. Devices 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 devices 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) or the Imitation Device feature in Scope.

Commands

Position, velocity, and torque (effort) can be commanded by using a CommandStruct. Each field expects a 1xN vector where N is the number of devices 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 devices
    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 devices 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 devices 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 torque (effort) to drive the output towards the origin (stiffness = 1 Nm / rad).

% Virtual spring on single device
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 devices, 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)
HebiUtils.sendWithRetry(group, 'gains', gains, 'persist', true)

However, we generally recommend explicitly setting the gains at the start of each script rather than relying on persisted gains. Gain structs can 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');
HebiUtils.sendWithRetry(group, 'gains', gains);

For gain scheduling use cases with frequent gain changes, we recommend combining gain commands in the same send call as other commands to guarantee that everything arrives 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);

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;
HebiUtils.sendWithRetry(group, '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');
HebiUtils.sendWithRetry(group, 'SafetyParams', safetyParams);

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 addBody API.

% Loading a robot description (hrdf) from file
kin = HebiUtils.loadHRDF('hrdf/A-2085-06G.hrdf');

Each HebiKinematics object is a serial chain of bodies that describe the kinematic relation of a robot. The chain gets defined starting from the base of the device.

Serial Chain Manipulators

A-2085-06G

Most configurations are serial chains such as the 6-DoF X-Series Manipulator kit A-2085-06G:

<?xml version="1.0" encoding="UTF-8"?>
<!-- X-Series Arm (6-DoF with Gripper) -->
<robot version="1.3.0">
  <actuator type="X8-9"/>
  <bracket type="X5HeavyRightOutside"/>
  <actuator type="X8-16"/>
  <link type="X5" extension="0.325" twist="pi"/>
  <actuator type="X8-9"/>
  <link type="X5" extension="0.325" twist="pi"/>
  <actuator type="X5-1"/>
  <bracket type="X5LightRight"/>
  <actuator type="X5-1"/>
  <bracket type="X5LightRight"/>
  <actuator type="X5-1"/>
  <end-effector type="X5Parallel"/>
</robot>

The file can be loaded from disk using the loadHRDF method. The resulting HebiKinematics object consists of multiple bodies that each represent either a rigid link or a dynamic joint element. The first body starts at the baseFrame and the last body represents the endEffector. The firstJointFrame is the combination of the baseFrame and all static bodies up until the first joint. Details about an existing configuration can be accessed at runtime using getBodyInfo for general body information, and getJointInfo for joint specific information.

loadHRDF 6dof

Multi-limbed Robots and Tree-Structures

A-2049-01

Some kits such as Daisy (A-2049-01) consist of multiple limbs that are attached to a shared chassis but effectively act independent of each other:

<?xml version="1.0" encoding="UTF-8"?>
<!--
    Leg Numbering / Chassis Coordinate convention:
    This should match ROS wheeled vehicle convention

    1 - - - 2           +x
        |                ^
    3 - - - 4            |
        |          +y <- o
    5 - - - 6             +z
-->
<robot version="1.3.0" description="Daisy">
    <rigid-body mass="0">
        <output rot="Rz(pi/6)"> <!-- leg 1 -->
            <rigid-body mass="0" output_trans="0.2375 0 0"/>
            <include path="daisyLeg-Left.hrdf"/>
        </output>
        <output rot="Rz(-pi/6)"> <!-- leg 2 -->
            <rigid-body mass="0" output_trans="0.2375 0 0"/>
            <include path="daisyLeg-Right.hrdf"/>
        </output>
        <output rot="Rz(pi/2)"> <!-- leg 3 -->
            <rigid-body mass="0" output_trans="0.1875 0 0"/>
            <include path="daisyLeg-Left.hrdf"/>
        </output>
        <output rot="Rz(-pi/2)"> <!-- leg 4 -->
            <rigid-body mass="0" output_trans="0.1875 0 0"/>
            <include path="daisyLeg-Right.hrdf"/>
        </output>
        <output rot="Rz(5*pi/6)"> <!-- leg 5 -->
            <rigid-body mass="0" output_trans="0.2375 0 0"/>
            <include path="daisyLeg-Left.hrdf"/>
        </output>
        <output rot="Rz(5*-pi/6)"> <!-- leg 6 -->
            <rigid-body mass="0" output_trans="0.2375 0 0"/>
            <include path="daisyLeg-Right.hrdf"/>
        </output>
        <output> <!-- chassis -->
            <include path="daisyChassis.hrdf"/>
            <end-effector/>
        </output>
    </rigid-body>
</robot>

Tree structures defined in a single HRDF can be parsed into a cell array of separate HebiKinematics objects representing each limb. Each one refers to the serial chain from the base frame to the desired endEffector indexed as they occur in the file.

% Loading a robot description (hrdf) from file
[~,~,limbs,jointIndices] = HebiUtils.loadHRDF('hrdf/Daisy.hrdf');

The additionally returned jointIndices list the actuator indices that are used in the chain. For example, Daisy consists of 18 actuators total, and leg 4 uses actuators 10-12.

loadHRDF Daisy

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. This is an important parameter for IK to work well. See the IK section of Core Concepts for more details. 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

Dynamics and inverse dynamics are important concepts that describe the relationship between accelerations and forces. Modeling required torques appropriately can significantly improve tracking while also improving stability. Please take a look at the Motion Control tutorial for an in-depth explanation with visual examples.

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 device. 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 of the first joint
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
imuFrame = kin.getFirstJointFrame();
gravityVec = imuFrame(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.

% 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 torques (or 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);
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);

Arm Control

We provide the HebiArm API for controlling groups of actuators in a manner similar to more traditional robot arms. Features include on-the-fly trajectory generation, built-in gravity and dynamics compensation, gripper support, and plugin features like impedance control. HebiArm requires knowledge about the group as well as the kinematic configuration:

% Arm wrapper for a 6 dof arm
kin = HebiUtils.loadHRDF('hrdf/A-2085-06G.hrdf');
group = HebiLookup.newGroupFromNames('Arm', {
    'J1_base'
    'J2_shoulder'
    'J3_elbow'
    'J4_wrist1'
    'J5_wrist2'
    'J6_wrist3'
});
arm = HebiArm(group, kin);

The first update call requests feedback, updates the state, and computes appropriate position, velocity, and torque (effort) commands. The commands can be modified by users before subsequently being sent out using the send function. While there are no goals set, the arm performs basic gravity compensation:

% Gravity compensation
arm.clearGoal();
while true
    arm.update();
    arm.send();
end

Setting desired goal joint-positions automatically creates an appropriate trajectory starting at the current state. The outgoing commands include positions, velocities, and torques (efforts) to compensate for joint accelerations and gravity.

% Execute a motion from the current point to the desired goal
arm.setGoal(jointPosition);
while ~arm.isAtGoal()
   arm.update();
   arm.send();
end

Arm Plugins

Additionally, we provide HebiArmPlugin for abstracting commonly used functionality. Plugins are called at the end of each update and may modify the state in arbitrary ways. Below are some of the built-in examples:

  • The EffortOffset plugin can be used to add torque offsets for arms with gas springs.

% add constant torque offset to account for shoulder spring
arm.plugins = {
    HebiArmPlugins.EffortOffset([0 -7 0 0 0 0])
}
  • The DoubledJointMirror plugin takes actuator commands and splits them with a parallel actuator such as in double-shoulder configurations

% Add an assisting parallel shoulder joint
shoulderIndex = 2;
secondShoulderJoint = HebiLookup.newGroupFromNames('Arm', 'J2B_shoulder');
arm.plugins = {
    HebiArmPlugins.DoubledJointMirror(shoulderIndex, secondShoulderJoint);
};
  • The ImpedanceController plugin adds impedance control on top of executing motions

% Add impedance controller that keeps the position while ignoring rotation
impedance = HebiArmPlugins.ImpedanceController();
impedance.gainsInEndEffectorFrame = true;
impedance.Kp = [500; 500; 500; 0; 0; 0];  % (N/m) or (Nm/rad)
impedance.Kd = [5; 5; 5; .0; .0; .0;];
arm.plugins{end+1} = impedance;

% Disable position / velocity commands and run purely on gravity
% compensated impedance control
arm.setGoal(desiredPosition);
while true
    arm.update();
    arm.cmdPos = [];
    arm.cmdVel = [];
    arm.send();
end

Mobile I/O

For use cases that require user interaction we recommend exploring the Mobile I/O iOS and Android app. It provides joystick-like axes and buttons as well as a rich set of sensor inputs. In MATLAB you can interface with the app using HebiMobileIO.

The API works similarly to HebiArm in that the update call updates the internal state, i.e., axes, buttons, and sensor feedback.

% Block until the device is found on the network
mobileIO = HebiMobileIO.findDevice('HEBI', 'mobileIO');

% Continuously display orientation until button 8 gets pressed
while ~mobileIO.getFeedbackIO().b8
    mobileIO.update();
    disp(mobileIO.getOrientation());
end

Devices that are connected using wireless networks can occasionally experience latency spikes or dropped packets. In cases where the app is used to control a robot, it is typically desirable to never block the control loop and to update the target only when there is new information. This can be done using a non-blocking overload.

% Block until the device is found on the network
mobileIO = HebiMobileIO.findDevice('HEBI', 'mobileIO');
quitDemoButton = 'b8'; % overload for more readable access

% Display updated feedback only when available
while ~mobileIO.getFeedbackIO().(quitDemoButton)

    % Try to update mobileIO
    [hasNewFeedback, timeSinceLastFeedback] = mobileIO.update('timeout', 0);
    if hasNewFeedback
        disp(mobileIO.getOrientation());
    elseif timeSinceLastFeedback > 3
        error('We have not received mobileIO feedback for 3 seconds!');
    end

    % Do robot control
    pause(1e-3);

end

HebiMobileIO also provides a way to setup the user interface as shown below. Please refer to the HebiMobileIO docs for more detailed information.

% Setup mobileIO UI
mobileIO = HebiMobileIO.findDevice('HEBI', 'mobileIO');
mobileIO.initializeUI();
mobileIO.setAxisValue([3 6], [-1 1]);
mobileIO.setButtonIndicator([1 8], true);
mobileIO.addText('B1 - Reset/re-align pose');
mobileIO.addText('A3 - Scale translation commands');
mobileIO.addText('A6 - Gripper Open/Close');
mobileIO.addText('B8 - Quit');

Trajectories

We provide the HebiTrajectoryGenerator API to create smooth trajectories between various sets of joint positions (waypoints) with specifiable velocity and acceleration constraints. The generated trajectories have the property that they smoothly accelerate and decelerate with "minimum jerk" and are similar to human movement. Please check the Trajectories tutorial for more information.

HebiTrajectoryGenerator is a low level API that is typically only needed for applications where HebiArm is not applicable or insufficient.

Creating Trajectories

Joint trajectories can be created using the newJointMove method. It computes minimum jerk trajectories that go through the specified waypoints (position, velocity, and acceleration) at the specified time. Unspecified velocity and acceleration constraints default to zero start and end conditions with unconstrained waypoints in between. The following code creates a sample trajectory between 6 position waypoints for a single actuator.

% Setup
trajGen = HebiTrajectoryGenerator();

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

Trajectory

The trajectory can be evaluated using getState.

% Evaluate state at specified time (scalar or vector)
[pos, vel, accel] = trajectory.getState(2);

TrajectoryGetState

Visualizing Trajectories

You can visualize the trajectory using the provided HebiUtils.plotTrajectory utility

% Visualize trajectory
HebiUtils.plotTrajectory(trajectory);

TrajectoryPlot

Alternatively, you can create a custom visualization 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
single joint trajectory

Determining Time

We additionally provide a heuristic for creating the time vector. Note that the current implementation is very basic and only takes into account velocity limits with an assumption that the velocity at each waypoint is zero.

The velocity limits can be specified by passing in velocity limits or a HebiKinematics object:

% Setup
velocityLimits = [-3 +3];
trajGen = HebiTrajectoryGenerator(velocityLimits);
trajGen.setMinDuration(0.5); % enforce minimum duration to reduce jitter
trajGen.setSpeedFactor(0.8); % reduce to 80% speed

% Create trajectory
positions = [0 pi 3 2.5 1 0];
trajectory = trajGen.newJointMove(positions);

The getWaypointTime method provides access to the determined time vector:

% Display determined time vector
time = trajectory.getWaypointTime();
TrajectoryWaypointTime

Creating the time vector can be annoying, so we have added a heuristic based on velocity limits. Note that the current heuristic is very simple and only

In order to simplify the generation of the time vector, we have added a simple heuristic based on velocity limits.

Linear Movements

Minimum-jerk joint trajectories generate smooth human-like motion, but the end effector generally does not move linearly through space.

JointTrajectory3d

Linear cartesian motions can be generated by using inverse kinematics to sample desired joint positions along a linear path between the points. For example, interpolating the above joint motion in 25mm intervals yields the approximately linear motion below:

LinearTrajectory3d

The details depend on the kinematic configuration and are typically application specific. We provide newLinearMove to serve as a starting point.

% Setup
kin = HebiUtils.loadHRDF('hrdf/A-2085-06G.hrdf');
trajGen = HebiTrajectoryGenerator(kin);

% Create joint waypoints
waypoints = [
    1.3958    1.0811    1.6582    7.6839    2.5466    9.4503
    1.0241    1.5928    1.4944    5.4796    3.0167    9.0366
];

% Compute linear trajectory w/ IK interpolations
trajectory = trajGen.newLinearMove(waypoints);

Executing Trajectories

Trajectories are designed to be executed/evaluated in a non-blocking manner so that users can dynamically react to changes in the environment such as hitting unforeseen obstacles or when following a moving target.

HebiTrajectoryGenerator.executeTrajectory is utility method provided to assist with simple movements in a blocking manner. However, this functionality is considered deprecated and we recommend using HebiArm instead.
% 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.

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

simpleLog

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

Visualizing logs

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, ':');

Additionally, we provide HebiUtils.plotLogs to help with basic feedback plots.

% Plot position (can be any field available in the log)
HebiUtils.plotLogs(data, 'position')

plotLogsPosition

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

This documentation refers to the Python 2.7.6 API, available at:

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

This documentation refers to the C++ 3.8.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 [cpp-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!

File Formats

Robot Config

The Robot Config file is a YAML-based format designed to store configuration information about a full system in a human-readable cross-API format.

The complete format, including past versions and example files using this format, is documented at https://github.com/HebiRobotics/robot-config.

Briefly, the format consists of a series of YAML elements that provide information about the used actuators, gains, kinematic structure, and optional plugins. For example, this snippet shows the robot config for a 6-DOF arm kit:

# X-Series 6-DoF Arm
version: 1.0
families: ["Arm"]
names: ["J1_base", "J2_shoulder", "J3_elbow", "J4_wrist1", "J5_wrist2", "J6_wrist3"]
hrdf: "../hrdf/A-2085-06.hrdf"

gains:
    default: "../gains/A-2085-06.xml"

user_data:
    # Default seed positions for doing inverse kinematics
    ik_seed_pos: [0.01, 1.0, 2.5, 1.5, -1.5, 0.01]

plugins:

  - type: GravityCompensationEffort
    name: gravComp
    enabled: true
    ramp_time: 2

  - type: DynamicsCompensationEffort
    name: dynamicsComp
    enabled: true
    ramp_time: 2

HRDF

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

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 on the "Gains" tab, and the gains in a file can be written to a module by using the "Load File" button. The "Load Defaults" button 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 Safety Parameters

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

XML Motor Driver Format

Motor driver configurations are stored in an XML format and can be loaded/sent from Scope or the hebi-config CLI. Each file specifies the configuration for one motor driver. For example, you can send files from the CLI with the following command,

hebi-config --name MD-210 --motor-driver-xml <file.xml> --persist --reset

The configuration is quite extensive and difficult to write from scratch. The easiest way to generate a file is to use Scope for the initial configuration and then click Save to file.

A small sample is in the snippet below. Note that you can use <xi:include> tags to share common configurations.

<?xml version="1.0"?>
<motor_driver_config xmlns:xi="http://www.w3.org/2001/XInclude">
    <xi:include href="configs/motors/EC90_220W.xml" parse="xml" />
    <xi:include href="configs/boards/SPI_80A_10VV.xml" parse="xml" />

    <driver_calibration>
        <winding_order value='ABC'/>
        <hall_order value='ABC'/>
    </driver_calibration>

    <control_config>
        <mode value='FOC_TORQUE_CONTROL'/>

        <foc_gains>
            <kp value='0.756'/>
            <ki value='0.0717'/>
            <i_clamp value='5'/>
        </foc_gains>
        <overmodulation value='1.15'/>

        <hall_config>
            <direction value='FORWARD'/>
            <velocity_filter_size value='1'/>
        </hall_config>

        <encoder_a2_config>
            <direction value='FORWARD'/>
            <velocity_filter_size value='10'/>
        </encoder_a2_config>

        <motor_encoder_selection value='A2'/>
    </control_config>

    <drivetrain_config>
        <gear_efficiency value='1'/>
        <gear_ratio value='1'/>
    </drivetrain_config>
</motor_driver_config>

You can find more complete examples in BLDC_EC-Flat60_24V_150W_CONFIG.xml and FOC_EC-Flat90_24V_220W_CONFIG.xml.

The full schema is described in motor_driver_schema.xml.

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.