ICON custom hardware module guide
Introduction to the hardware abstraction layer
ICON's hardware abstraction layer ("HAL") is responsible for translating commands from ICON's real-time control layer ("RTCL") into hardware-specific commands. It is also responsible for reporting hardware status to the RTCL.
The HAL is extensible by means of custom hardware modules. By writing a custom hardware module, you can add support for new robotic hardware to ICON.
A custom hardware module is an executable binary that runs in a dedicated kubernetes pod on the real-time PC ("RTPC"). The hardware module implementation is a C++ class containing methods that execute in a real-time context.
Communication between the ICON server and the custom hardware module is established through hardware interfaces. These hardware interfaces are flatbuffer objects stored in shared memory. The custom hardware module implementation reads and writes to these objects directly using flatbuffer APIs. Therefore, we recommend that anyone writing custom hardware modules be familiar with flatbuffers.
Prerequisites
Before you can start the development of your custom hardware module this guide expects the following:
- The hardware you want to control is physically connected to an IPC. In a multi device cluster, it must be connected to the cluster's real-time PC.
- Your development machine has a direct Ethernet connection to the same IPC (same network as the IPC's uplink port)
- You're familiar with the name of the real-time PC. In a multi device cluster the name of the real-time PC will be different from the IPC's name. Intrinsic RTPCs are shipped with a label on the front. Contact Intrinsic for help if you don't know the right value to use.
The next step is to setup the development environment on your development machine.
Development environment
This section explains how to setup the local development environment.
Installation requirements
Follow the guide on how to set up the development environment if you haven't already.
Additionally you will need to deploy your custom hardware module as an Intrinsic service so we recommend familiarizing yourself with the creation of custom services.
From now on, you should be running Visual Studio Code with your project folder opened inside the provided Dev Container.
Prepare the Bazel workspace
Use inctl to create a new bazel workspace.
inctl bazel init --sdk_repository=https://github.com/intrinsic-ai/sdk.git --sdk_version latest
This makes all ICON extension APIs available for this folder, including the custom hardware module API.
You can confirm a correct setup by creating a list of all build targets of the SDK:
bazel query @ai_intrinsic_sdks//...
Build and deploy a hardware module
The easiest way to begin working with hardware modules is to compile and deploy an existing one. The SDK contains the loopback hardware module which only reports back the commanded joint position every cycle. When adding support for custom hardware, we recommend using this loopback hardware module as a starting point.
A complete example of the build targets necessary for creating a hardware module can be found in the SDK.
In general to create your own custom hardware module you will need to recreate a similar set of targets. These have the following roles:
-
A
cc_libraryto create a library containing your custom hardware module implementation. This will be an implementation of the `HardwareModuleInterface abstract base class. -
A
hardware_module_binarybuild rule to link a custom hardware module implementation with the hardware module runtime, creating a binary executable. -
A
hardware_module_imagebuild rule that packages the hardware module binary into a container image, suitable for deployment with an Intrinsic service asset. -
A
hardware_module_manifestbuild rule that defines theintrinsic_servicemetadata. This creates aServiceManifesttextproto that is configures the service to be able to communicate with ICON via shared memory and have real-time performance. -
An
intrinsic_servicerule that creates the service which packages the hardware module image.
With the above rules you can build and deploy your hardware module as with any other Intrinsic service asset. For this please refer to the latest instructions on installing intrinsic_service assets.
Developer's guide
This section provides information for developers, that want to develop their own hardware module.
The hardware module interface defines a series of methods which may be triggered by the real-time framework. These include non-realtime methods which are triggered to initialize and to clear faults, as well as real-time methods which handle the control loop.
This guide only provides complementary information to the detailed documentation of the hardware module interface provided in the SDK. We recommend thoroughly reading that documentation before implementing a custom hardware module.
Initialization and Fault Handling
Bringing up a hardware module will likely involve various operations to connect to and enable operations according to the robot specific communication interface.
Given the robot specific requirements implementations will vary but here we provide some general suggestions on what code to place in particular methods of the implementation.
-
Init: This is the first method used to initialize the hardware module. It is recommended to put the initialization of hardware interfaces (see the next section for additional details) and other similar operations which are unlikely to fail in this method. In addition this is where the hardware module receives its configuration through theHardwareModuleInitContext. Any failure here is considered fatal and requires a complete service restart to recover from. We recommend validating the configuration here but putting code which may fail but does not require reconfiguration intoPrepare. -
Prepare: This is called afterInitfrom the control framework and after this returns the hardware module should be ready to immediately send and receive real-time method calls such asActivate. A common strategy is to use this to create the real-time threads and activate communication with the robot controller. Similar toInitany failure here is considered fatal and requires a complete service restart to recover from. Thus for failures which can be recovered without a restart it is preferable not to return an error and instead wait till a call to EnableMotion is made. This allows clearing the error withClearFaults. -
Activate: This call must return in real-time and triggers the start of the realtime cyclical communication between ICON and the hardware module. If the module is clock-driving then it must begin ticking the clock immediately after this call. AfterActivateis called ICON will callReadStatusevery cycle. -
EnableMotionandEnabled: These methods are used to transition to a state in which ICON is actively controlling the module via calls toApplyCommand.EnableMotionmay block but after which the module must be ready to receive a call toEnabledwhich will immediately precede theReadStatusandApplyCommandcalls made in the first cycle in whichApplyCommandis called. -
DisableMotionandDisabled: These methods are used to transition to a state in which ICON is no longer controlling the module via calls toApplyCommand.DisableMotionmay block but after which the module must be ready to receive a call toDisabledafter which it will no longer receive any commands. -
ClearFaults: This method is used to recover from all non-fatal faults. It can block but after a successfuly call to it the module should expect calls toReadStatus. This is a very useful method since it is the fastest method for recovering from a fault through the Flowstate frontend.
Hardware interface registration
Hardware interfaces are used to communicate and exchange data with the RTCL. Mutable hardware interfaces are used to report hardware status to the RTCL. Read-only hardware interfaces are used to receive commands from the RTCL.
A hardware module must declare and instantiate each of its hardware interfaces. Doing so creates a flatbuffer object in shared memory and returns a handle to it. The hardware module should hold onto this handle.
Our hardware module must report its joint positions. For that, advertise an
intrinsic_fbs::JointPositionState hardware interface in our module. As a class
member, add a mutable interface handle:
private:
// HAL interface handles.
xfa::icon::MutableHardwareInterfaceHandle<intrinsic_fbs::JointPositionState>
joint_position_state_;
Then, in your module's Init method, advertise the interface:
absl::Status LoopbackHardwareModule::Init(
HardwareInterfaceRegistry& interface_registry, const ModuleConfig& config) {
ASSIGN_OR_RETURN(
joint_position_state_,
interface_registry.AdvertiseMutableInterface<JointPositionState>(
"position_state", kNumDofs));
The hardware module needs to receive a joint position command, so you advertise
an intrinsic_fbs::JointPositionCommand hardware interface. As a class member,
add a read-only interface handle:
private:
xfa::icon::HardwareInterfaceHandle<intrinsic_fbs::JointPositionCommand>
joint_position_command_;
Then, in your module's Init method, advertise the interface:
absl::Status LoopbackHardwareModule::Init(
HardwareInterfaceRegistry& interface_registry, const ModuleConfig& config) {
ASSIGN_OR_RETURN(
joint_position_command_,
interface_registry.AdvertiseInterface<JointPositionCommand>(
"position_command", kNumDofs));
The hardware module can now communicate with the RTCL by writing to
joint_position_state and reading from joint_position_command. Note that the
arm part expects additional hardware interfaces. See the
/intrinsic/icon/hardware_modules/loopback/loopback_hardware_module.cc for a
complete example.
Hardware module registration
In the .cc file, register the interfaces of the hardware module.
Supported interfaces are listed in icon/hal/default_hardware_interfaces.h.
INTRINSIC_ADD_HARDWARE_INTERFACE(intrinsic_fbs::JointPositionCommand,
intrinsic_fbs::BuildJointPositionCommand,
"intrinsic_fbs.JointPositionCommand")
Finally, to export a hardware module, register it using the
REGISTER_HARDWARE_MODULE macro. This should be placed in the outer scope at
the bottom of your .cc file.
REGISTER_HARDWARE_MODULE(loopback_module::LoopbackHardwareModule)
The real-time control loop
The Init function of the hardware module gets called immediately when the
hardware module starts. This function allows to setup all necessary
communication to the actual robot hardware and as described earlier sets up the
communication channels to and from ICON. However, given its multi-process
architectural setup, ICON and the hardware module may start with a slight delay.
When both processes - ICON and the hardware module - are fully setup and ready
to enter a real-time loop, ICON calls Activate() on the hardware module.
RealtimeStatus Activate();
This function signals the hardware module that ICON is ready to periodically call ReadStatus(). Equivalently, before ICON stops, it
calls Deactivate() on the hardware module. It is important to note that before
the call to Activate() occurs, the hardware module is responsible to keep a
healthy connection to the hardware as ICON might not have started yet. Only after a successful call to Activate(), ICON has correctly started the real-time loop.
The ICON server runs a real-time control loop. Every control cycle, the following steps are performed in order (bold steps are customizable):
| Control cycle sub-steps |
|---|
Hardware module: ReadStatus |
| (context switch to ICON server process) |
Once the ICON server is enabled, the real-time loop is extended by the controller loop which computes new command values through its respective actions:
| Control cycle sub-steps |
|---|
Hardware module: ReadStatus |
| (context switch to ICON server process) |
Action: Sense |
Action: Control |
| Reaction handling |
| (context switch to custom hardware module process) |
Hardware module: ApplyCommand |
The implementations of these steps need to be real-time safe. That is, they must always finish computation within a single control cycle, even in error cases. 1000Hz is a common control frequency, which means all steps should take well under one millisecond to finish, even in exceptional or error cases. Follow the real-time C++ programming style to implement a real-time safe hardware module.
Hardware module update
The hardware module's ReadStatus method is invoked at the beginning of every
control cycle. The implementation should update all mutable hardware interfaces
with the latest state received from hardware.
The hardware module's ApplyCommand method is invoked at the end of every
control cycle. The implementation should read from the read-only hardware
interfaces and apply the appropriate commands to hardware.
Because a hardware module's ApplyCommand is followed by the next cycle's
ReadStatus, it is possible to write to mutable hardware interfaces during
ApplyCommand. For example, this is what the loopback hardware module does:
RealtimeStatus LoopbackHardwareModule::ApplyCommand() {
for (size_t i = 0; i < kNumDofs; ++i) {
joint_position_state_->mutable_position()->Mutate(
i, joint_position_command_->position()->Get(i));
joint_velocity_state_->mutable_velocity()->Mutate(i, 0.0);
joint_acceleration_state_->mutable_acceleration()->Mutate(i, 0.0);
}
return OkStatus();
}
RealtimeStatus LoopbackHardwareModule::ReadStatus() {
// NO-OP, Status is updated already through `ApplyCommand`.
return OkStatus();
}
On the other hand, reading from read-only hardware interface during ReadStatus
obtains the previous cycle's commands, which may be stale.
Shared Libraries
For building hardware module binaries and images it is recommended to use statically linked libraries. In cases where this is not possible, shared libraries can be added to the hardware module image via additional container layers. The Intrinsic SDK provides helper build rules for constructing container layers which can be added to the hardware module image.
You can import a shared library through the bazel cc_import rule. This allows you to link the shared library as a dependency to your hardware module library. Then, the compiled shared library files need to be added to the hardware module image as layers constructed using the container_layer rule. For a hypothetical shared library libshared:
cc_import(
name = "libshared",
hdrs = [
"inc/libshared.h",
],
shared_library = "lib/libshared.so",
)
cc_library(
name = "my_hardware_module",
...
deps = [
":libshared",
...
]
)
hardware_module_binary(
name = "my_hardware_module_main",
hardware_module_lib = ":loopback_hardware_module",
)
container_layer(
name = "libshared-runfiles",
directory = "/usr/lib",
files = [
":lib/libshared.so",
],
)
hardware_module_image(
name = "my_hardware_module_image",
hardware_module_binary = "my_hardware_module_main",
layers = [":libshared-runfiles"],
)
Limitations
At this time, the HAL only supports a fixed and limited set of hardware
interfaces to the RTCL. The list of supported interfaces is in
icon/hal/default_hardware_interfaces.h.