ROBROS

Human First,

line

Always

Dev Center

Last Updated On: 2026-07-10

ControlMode

This page reorganizes the former "Low Level Control" page into Control Mode, summarizing the meaning and switching methods of each control mode supported by IGRIS-C, as well as the message interfaces required for low-level control.

⚠️ The code on this page targets the v2.1.1 SDK. In 2.1.1, topic names (rt/ removed), type names, constant names (enums → uint8 constants) and the BMS → PDU terminology changed, and a 2.1.0 SDK client cannot communicate with a 2.1.1 robot (see SDK Overview).

  • IGRIS-C's behavior hierarchy is switched via Control Mode. Mode-switch commands are sent to the controller through the SDK's IgrisC_Client::SendControlModeCommand() (internally a ControlModeCommandRequest Request/Response), and the controller maps this to an internal RobotControlMode to change the control loop's behavior.

struct ControlModeCommandRequest_Request {
  std_msgs/Header header;
  string          request_id;    // ID for matching requests
  uint8           command_type;  // Mode to switch to (see list below, CONTROL_MODE_CMD_* constants)
  string          preset_id;     // MOTION_PRESET only
  boolean         is_cyclic;     // MOTION_PRESET_CYCLIC_TOGGLE only
};
// Response: ControlModeCommandRequest_Response { header, request_id, success, message, error_code(0=success) }

1. List of Control Modes

command_type (uint8 constants, 19 types total, values 0–18). Controller behavior is based on robotHandler dispatch.

#

command_type

Controller mapping/behavior

Layer

0

MOTION_PRESET

startMotionPreset(preset_id)

High-Level

1

MOTION_PRESET_CYCLIC_TOGGLE

setMotionPresetCyclic(is_cyclic)

High-Level

2

JOINT_POSITION_HOLD

→ JOINT_HOLD_MODE

-

3

MOTION_STOP

holdCurrentPosition()

High-Level

4

WALKMODE_ON

→ WALK_MODE

-

5

LOW_LEVEL_JOINT_CONTROL

→ LOW_LEVEL_MODE

Low-Level

6

LOW_LEVEL_WALKMODE_ON

→ LOW_LEVEL_WALK_MODE

Low-Level

7

HIGH_LEVEL_WALKMODE_ON

→ HIGH_LEVEL_WALK_MODE

High-Level

8

HIGH_LEVEL_JOINT_CONTROL

→ HIGH_LEVEL_MODE

High-Level

9–18

CUSTOM_MODE_1 – CUSTOM_MODE_10

(internal use only)

Not provided

command_type values are uint8 constants of the form igris_c_sdk::srv::dds_::ControlModeCommandRequest_Request_Constants::CONTROL_MODE_CMD_<NAME> (e.g., CONTROL_MODE_CMD_MOTION_PRESET). In Python they are igc_sdk.CONTROL_MODE_CMD_<NAME>.

⚠️ CUSTOM_MODE_1–CUSTOM_MODE_10 (9–18) are not currently provided to users. These are internal/extension-only entries with reserved slots in the enum only — do not use them when working with the SDK.

The two modes below cannot be requested by command; the controller enters them on its own when their conditions are met (details in Section 2, "Automatic Safety Modes").

Controller mode

Entered when

control_state

PASSIVE_MODE

Right after startup, and automatically whenever power or torque turns OFF or the E-stop is pressed (renamed from 2.1.0's TORQUE_OFF_MODE)

IDLE (NOT_READY while power/torque is OFF or the E-stop is engaged)

DAMPING_MODE

Entered automatically when the walking policy detects a fall (new in 2.1.1)

STOP

2. Description of Each Mode

Low-Level Control

  • The SDK directly commands each motor's targets (q·dq·tau·kp·kd) via lowcmd. In this mode, control loop responsibility resides externally (with the SDK). (See section 5 for the message interface.)

    • LOW_LEVEL_JOINT_CONTROL (5) — LOW_LEVEL_MODE. Direct joint control via SDK LowCmd.

    • LOW_LEVEL_WALKMODE_ON (6) — LOW_LEVEL_WALK_MODE. Walking mode based on low-level control.

  • Low-level commands in MS (motor space) are projected onto the parallel links' motor-space safe set before they are applied. Measured values reported in lowstate are not projected, so depending on the actual posture a joint value beyond its limit can be reported.

  • External handcmd is applied only in LOW_LEVEL_MODE and LOW_LEVEL_WALK_MODE (the HIGH_LEVEL modes use the preset's hand motion; all other modes ignore it).

High-Level Control

  • The controller's built-in logic receives higher-level commands and generates/maintains motion. Motion presets run at this layer; a preset requested from another mode switches to this layer automatically and then starts (see "Motion Preset" below).

    • HIGH_LEVEL_JOINT_CONTROL (8) — HIGH_LEVEL_MODE. Controller's high-level joint control.

    • HIGH_LEVEL_WALKMODE_ON (7) — HIGH_LEVEL_WALK_MODE. High-level walking mode.

Walk

  • WALKMODE_ON (4) — WALK_MODE. Enters walking mode.

  • Walk velocity commands are sent on the cmd_vel topic (geometry_msgs/TwistStamped, the same on every lane). Examples: cyclonedds_twist_publisher, twist_publisher.py, ros_twist_publisher.

  • ⚠️ From 2.1.1, entering WALKMODE_ON no longer moves the arms slowly first (the 2.1.0 slow start of 0.4 rad/s for 3 s is gone). The walking policy controls the whole body immediately, so clear the area around the robot — especially the arms' range of motion — before requesting it. WALKMODE_ON is not rejected in DAMPING_MODE either, so make sure the robot is standing upright before requesting it.

Motion Preset

  • MOTION_PRESET (0) — Plays back a predefined motion specified by preset_id (startMotionPreset). Runs in HIGH_LEVEL_MODE or HIGH_LEVEL_WALK_MODE. When requested from another mode, the robot switches automatically — to HIGH_LEVEL_WALK_MODE from LOW_LEVEL_WALK_MODE or WALK_MODE, otherwise to HIGH_LEVEL_MODE — and then starts the preset. In the stopped state (below) only HOME can be run. Populate the preset_id argument in the request.

  • MOTION_PRESET_CYCLIC_TOGGLE (1) — Toggles whether the preset repeats cyclically (setMotionPresetCyclic). Uses the is_cyclic argument in the request.

Stop / Hold

  • JOINT_POSITION_HOLD (2) — JOINT_HOLD_MODE. Maintains (holds) the current joint positions. When the request succeeds, the robot enters the stopped state described below.

  • MOTION_STOP (3) — Stops the ongoing motion and maintains the current posture (holdCurrentPosition). Only supported from HIGH_LEVEL_MODE/HIGH_LEVEL_WALK_MODE.

Stopped state (Motion-Stop latch)

  • When MOTION_STOP or JOINT_POSITION_HOLD succeeds, the robot latches into the stopped state, and control_state is STOP (2.1.0 reported HIGH_MOTION_STANDBY/IDLE).

  • In the stopped state, the only accepted requests are MOTION_PRESET "HOME" and switches to the safe-stop modes (JOINT_HOLD_MODE, PASSIVE_MODE, DAMPING_MODE; PASSIVE and DAMPING are entered automatically). Every other mode switch or preset is rejected, with the reason in the response message. (2.1.0 accepted only HOME.)

  • Starting the HOME preset releases the stopped state. If needed, the robot first switches to HIGH_LEVEL_MODE (or HIGH_LEVEL_WALK_MODE) automatically.

Automatic Safety Modes (DAMPING / PASSIVE)

  • DAMPING_MODE — a safe-stop mode with kp=0 that keeps only kd. It cannot be requested from the SDK or the app; the walking policy enters it automatically when it detects a fall in WALK_MODE, HIGH_LEVEL_WALK_MODE or LOW_LEVEL_WALK_MODE (default policy: pelvis tilt above 0.8 rad, or above 0.4 rad with angular velocity above 1.57 rad/s). control_state is STOP.

  • ⚠️ Whether DAMPING_MODE is entered depends only on the tilt criteria above, not on the hoist. Even while hanging on a hoist, the robot switches and goes limp once its tilt exceeds them.

  • PASSIVE_MODE — a standby mode that outputs no command (2.1.0's TORQUE_OFF_MODE). Mode switches are accepted only while body power and torque are ON and the E-stop is released; a request in any other condition is rejected and the controller switches to PASSIVE_MODE.

Custom — Currently Not Provided

  • CUSTOM_MODE_1–CUSTOM_MODE_10 (9–18) — Internal/extension-only slots not currently provided to users. These are defined in the enum but are not modes callable by SDK users, so do not use them in normal control.

  • Note: the safety profile of these slots changed in 2.1.1. The waist and ankle parallel links use SATURATE + SOFT and the wrists CLIP + HARD (2.1.0 used CLIP + HARD everywhere).

3. Mode Switching API (SDK)

Mode switching is performed via the single method IgrisC_Client::SendControlModeCommand().

igris_c_sdk::msg::dds_::ServiceResponse_ SendControlModeCommand(
    uint8_t                command_type,       // ControlModeCommandRequest_Request_Constants::CONTROL_MODE_CMD_*
    const std::string&     preset_id = "",     // MOTION_PRESET only
    bool                   is_cyclic = false,  // MOTION_PRESET_CYCLIC_TOGGLE only
    int                    timeout_ms = 5000);

Usage example (based on cyclonedds/cyclonedds_service.cpp):

using namespace igris_c_sdk::srv::dds_;   // ControlModeCommandRequest_Request_Constants

// Execute motion preset "HOME" (at the HIGH_LEVEL layer)
res = client.SendControlModeCommand(
    ControlModeCommandRequest_Request_Constants::CONTROL_MODE_CMD_MOTION_PRESET, "HOME", false, 60000);

// Hold current position
res = client.SendControlModeCommand(
    ControlModeCommandRequest_Request_Constants::CONTROL_MODE_CMD_JOINT_POSITION_HOLD, "", false, 60000);

// Stop ongoing motion
res = client.SendControlModeCommand(
    ControlModeCommandRequest_Request_Constants::CONTROL_MODE_CMD_MOTION_STOP, "", false, 60000);

// Switch to low-level joint control mode (use lowcmd afterward)
res = client.SendControlModeCommand(
    ControlModeCommandRequest_Request_Constants::CONTROL_MODE_CMD_LOW_LEVEL_JOINT_CONTROL, "", false, 60000);

Check the result via the returned ServiceResponse's success / error_code (0=success) / message.

  • Requesting the mode the robot is already in returns success=false ("Control mode is already …").

  • On a mode change, the command blends from the previous mode's command to the new one over 0.5 s. Switches into or out of PASSIVE_MODE, JOINT_HOLD_MODE or DAMPING_MODE take effect immediately.

4. Service Topics Summary

Request/Response service channels used by the SDK client:

Function

Service Topic

SDK Method

PDU/motor initialization

service/pdu_init

InitPdu()

Torque ON/OFF

service/torque

SetTorque()

Hand initialization

service/hand_init

InitHand()

Control mode switching

service/control_mode

SendControlModeCommand()

Mujoco simulation control (sim only)

service/mujoco_sim

SendMujocoSimCmd()

On the cyclonedds lanes (cyclonedds_native / cyclonedds_ros_compatible), each service operates as a pair: <topic>/request (publishes the request) · <topic>/response (subscribes to the response). Example: service/pdu_init/request · service/pdu_init/response. (The table above shows the base topic.)

  • How services are offered depends on the robot's transport mode (SDK Overview, Section 3). On the cyclonedds lanes they are the request/response topic pairs above, called with IgrisC_Client. These pairs are not ROS 2 services, so they do not appear in ros2 service list, even on cyclonedds_ros_compatible.

  • On the ros lane, real rclcpp services with the same names are offered (/<ns>/service/pdu_init etc., types igris_c_sdk/srv/PduInitCmd · TorqueCmd · HandInitRequest · ControlModeCommandRequest · MujocoSimCmd); call them with the ros_service example in ros2/.

  • Response types are per service (PduInitCmd_Response_, TorqueCmd_Response_, HandInitRequest_Response_, ControlModeCommandRequest_Response_, MujocoSimCmd_Response_). They all carry the same fields — header, request_id, success, message, error_code — and the IgrisC_Client methods return them as the common igris_c_sdk::msg::dds_::ServiceResponse_.

Low-level streams: lowcmd (LowCmd, commands) / lowstate (LowState, status).

5. Low-Level Control Message Interface

IGRIS-C Topic List

  • Low-level Cmd & State (high frequency): lowcmd / lowstate

  • Service State (low frequency): pdustate / robotstate

Common (igris_c_sdk::msg::dds_ / igris_c_sdk::srv::dds_)

const uint32 N_JOINTS = 31; // Fixed DOF Num

// Kinematic Space (MS=Motor Space/AB, PJS=Parallel Joint Space/PR)
// uint8 constants, not an enum (LowCmd_Constants)
uint8 KINEMATIC_MODE_MS  = 0;
uint8 KINEMATIC_MODE_PJS = 1;

// Motor control is HYBRID mode only. (The former MotorMode { CURRENT, HYBRID } enum has been removed.)

Command Side

struct MotorCmd {            // HYBRID only
  uint16 id;   // MS: motor index / PJS: virtual joint index
  float  q;    // Position (rad)
  float  dq;   // Velocity (rad/s)
  float  tau;  // Feedforward Torque (Nm)
  float  kp;   // kP
  float  kd;   // kD
};

struct LowCmd {
  std_msgs/Header header;
  uint8           kinematic_modes[5];  // Per parallel-link group: waist / L_ankle / R_ankle / L_wrist / R_wrist
  MotorCmd        motors[31];          // Fixed length, 31 DOF
};
// Usage example: set all groups to the same mode
cmd.kinematic_modes().fill(igris_c_sdk::msg::dds_::LowCmd_Constants::KINEMATIC_MODE_PJS);

State Side

// imu_state is the standard sensor_msgs/Imu, not a custom struct:
//   std_msgs/Header header;
//   Quaternion  orientation;                  // named x/y/z/w fields
//   double      orientation_covariance[9];
//   Vector3     angular_velocity;             // rad/s
//   double      angular_velocity_covariance[9];
//   Vector3     linear_acceleration;          // m/s^2
//   double      linear_acceleration_covariance[9];
// There is no rpy field - compute it from orientation if you need it.

struct MotorState {
  float  q;             // Position (rad)
  float  dq;            // Velocity (rad/s)
  float  tau_est;       // Estimated Torque (Nm)
  int16  temperature;   // Motor Temperature (°C)
  uint32 status_bits;   // fault/limit bit flags; values are MotorState constants
};

struct JointState {
  float  q;            // PJS Position (rad)
  float  dq;           // PJS Velocity (rad/s)
  float  tau_est;      // PJS Estimated Torque (Nm)
  uint32 status_bits;  // Status Bits
};

struct LowState {
  std_msgs/Header  header;
  sensor_msgs/Imu  imu_state;         // Base IMU state (standard type)
  MotorState motor_state[N_JOINTS];   // MS raw state
  JointState joint_state[N_JOINTS];   // PJS derived state
};
  • The error flags in status_bits are provided as MotorState message constants (MOTOR_STALL_ERROR, OVER_CURRENT_ERROR, etc.): igris_c_sdk::msg::dds_::MotorState_Constants::* in C++, igc_sdk.MOTOR_STALL_ERROR etc. in Python. They are bit flags, so test them with (status_bits & FLAG), not equality.

  • IMU angles are in radians. If you need roll/pitch/yaw, compute them from the orientation quaternion.

All types are defined in the igris_c_sdk::msg::dds_ / igris_c_sdk::srv::dds_ namespaces (the IDL's module). In actual use, reference them as igris_c_sdk::msg::dds_::TypeName_. The standard types are std_msgs::msg::dds_::Header_ and sensor_msgs::msg::dds_::Imu_.

6. System & Mode Control API (Initialization / Torque / Hand)

#include <igris_c_sdk/channel_factory.hpp>
#include <igris_c_sdk/igris_c_client.hpp>
using namespace igris_c_sdk;

int main() {
    int domain_id = 0;
    ChannelFactory::Instance()->Init(domain_id, "igris_c_IG01");  // domain + robot unit namespace (see SDK Overview section 4)
    if (!ChannelFactory::Instance()->IsInitialized()) { return 1; }

    IgrisC_Client client;
    client.Init();
    client.SetTimeout(10.0f);  // 10-second timeout
    // ... subsequent service calls
}

PDU / Motor Initialization

using namespace igris_c_sdk::srv::dds_;   // PduInitCmd_Request_Constants
igris_c_sdk::msg::dds_::ServiceResponse_ res = client.InitPdu(PduInitCmd_Request_Constants::PDU_INIT, 30000);   // PDU power ON
res = client.InitPdu(PduInitCmd_Request_Constants::MOTOR_INIT, 30000);                                          // Motor initialization only
res = client.InitPdu(PduInitCmd_Request_Constants::PDU_AND_MOTOR_INIT, 30000);                                  // Power ON + motor (full sequence)
res = client.InitPdu(PduInitCmd_Request_Constants::PDU_INIT_NONE, 30000);                                       // PDU power OFF
  • init_type (uint8, constants in PduInitCmd_Request_Constants) — Values actually handled by the bridge:

    • PDU_INIT_NONE (0) — PDU power OFF (setPduPowerAsync(false))

    • PDU_INIT (1) — PDU power ON

    • MOTOR_INIT (2) — Motor initialization

    • PDU_AND_MOTOR_INIT (3) — Power ON + motor initialization (full sequence)

    • PDU_OFF (4) — PDU power OFF (handled the same as PDU_INIT_NONE)

  • timeout_ms — Timeout in ms (default 5000)

  • Returned ServiceResponse_ — request_id, success, message, error_code (0=success)

Torque Control

using namespace igris_c_sdk::srv::dds_;   // TorqueCmd_Request_Constants
igris_c_sdk::msg::dds_::ServiceResponse_ res = client.SetTorque(TorqueCmd_Request_Constants::TORQUE_ON, 30000);
res = client.SetTorque(TorqueCmd_Request_Constants::TORQUE_OFF, 30000);
  • torque (uint8, constants in TorqueCmd_Request_Constants) — TORQUE_NONE(0, reserved) / TORQUE_ON(1) / TORQUE_OFF(2)

  • timeout_ms — Timeout in ms (default 5000)

  • Requesting TORQUE_ON while torque is already on does nothing and returns success (success=true). (2.1.0 warned and re-ran the torque-on sequence.)

Hand Initialization

igris_c_sdk::msg::dds_::ServiceResponse_ res = client.InitHand(30000);   // End-effector calibration/homing

(For detailed hand control, see the Hand page)

PDU Status Query (pdustate, low frequency)

header, body_power(RELAY_OFF/ON), legs_power, estop, connect, battery(percent, 0–100), pdu_init_state

  • pdu_init_state — bit0 = PDU, bit1 = motor: PDU_NOT_INITIALIZED(0) / PDU_INITIALIZED(1) / MOTOR_INITIALIZED(2) / BOTH_INITIALIZED(3). The constants are in igris_c_sdk::msg::dds_::PduState_Constants (Python igc_sdk.BOTH_INITIALIZED etc.).

Control State Query (robotstate)

The state field (control_state) of RobotState is a summary computed from the current controller mode and the preset playback status. The constants are igris_c_sdk::msg::dds_::RobotState_Constants::ROBOT_STATE_* (Python igc_sdk.ROBOT_STATE_*), and environment is ROBOT_ENV_REAL(0) / ROBOT_ENV_SIM(1).

Value

control_state

Description

0

NOT_READY

Body power or torque is off, or the E-stop is engaged (regardless of mode)

1

IDLE

No preset in HIGH_LEVEL_MODE, or PASSIVE_MODE

2

STOP

Stopped state (MOTION_STOP latch), JOINT_HOLD_MODE, DAMPING_MODE

3

LOW

LOW_LEVEL_MODE

4

HIGH_MOTION_ACTIVE

Preset playing in HIGH_LEVEL_MODE

5

HIGH_MOTION_STANDBY

Preset loaded and waiting in HIGH_LEVEL_MODE

6

WALK_LOW

LOW_LEVEL_WALK_MODE

7

WALK_HIGH_MOTION_ACTIVE

Preset playing in HIGH_LEVEL_WALK_MODE

8

WALK_HIGH_MOTION_STANDBY

HIGH_LEVEL_WALK_MODE, no preset playing

9

WHOLE_BODY_CONTROL

Reserved (not currently used)

10

WALK

WALK_MODE (constant added in 2.1.1)

11–20

CUSTOM_1 – CUSTOM_10

CUSTOM_MODE_1 – CUSTOM_MODE_10 (constants added in 2.1.1)

7. Python Binding

Installing the dist/*.whl from the igris_c_sdk_public repository into a Python environment allows usage with the same getter/setter pattern as C++. Examples are located in the python folder.

  • Message class names end in _ as in C++ (igc_sdk.LowCmd_() etc.). Constants are module-level constants (igc_sdk.PDU_INIT, igc_sdk.TORQUE_ON, igc_sdk.CONTROL_MODE_CMD_*, igc_sdk.KINEMATIC_MODE_PJS, etc.).

  • In Python, timeout_ms is a required argument of InitPdu / SetTorque / InitHand (no default).

import igris_c_sdk as igc_sdk

channel_instance = igc_sdk.ChannelFactory.Instance()
channel_instance.Init(0, "igris_c_IG01")   # domain_id + robot unit namespace (see SDK Overview section 4)
# if the robot is on cyclonedds_ros_compatible:
# channel_instance.Init(0, "igris_c_IG01", "", igc_sdk.parse_topic_naming("ros_compatible"))

client = igc_sdk.IgrisC_Client()
client.Init()
client.SetTimeout(20.0)

Real-time lowstate Subscription

lowstate_subscriber = igc_sdk.LowStateSubscriber("lowstate", igc_sdk.QosProfile.SensorData())

def lowstate_callback(msg):
    imu = msg.imu_state()
    q    = imu.orientation()            # q.x() q.y() q.z() q.w()
    gyro = imu.angular_velocity()
    acc  = imu.linear_acceleration()    # no rpy: compute it from q if you need it
    m0  = msg.motor_state()[0]
    print(m0.q(), m0.dq(), m0.tau_est(), m0.temperature(), m0.status_bits())

lowstate_subscriber.init(lowstate_callback)

Service Calls (init / torque / control mode)

# PDU / Motor
client.InitPdu(igc_sdk.PDU_INIT, 5000)
client.InitPdu(igc_sdk.PDU_AND_MOTOR_INIT, 30000)

# Torque
client.SetTorque(igc_sdk.TORQUE_ON, 5000)

# Control mode (same as C++ SendControlModeCommand)
client.SendControlModeCommand(
    igc_sdk.CONTROL_MODE_CMD_MOTION_PRESET, "HOME", False, 60000)
client.SendControlModeCommand(
    igc_sdk.CONTROL_MODE_CMD_JOINT_POSITION_HOLD, "", False, 60000)

# Hand
client.InitHand(5000)

Sending LowCmd (e.g., controlling neck yaw/pitch motors)

lowcmd_publisher = igc_sdk.LowCmdPublisher("lowcmd", igc_sdk.QosProfile.SensorData())
lowcmd_publisher.init()

low_cmd_msg = igc_sdk.LowCmd_()
for i in range(0, 31):
    motor_cmd = low_cmd_msg.motors()[i]
    motor_cmd.id(i); motor_cmd.q(0.0); motor_cmd.dq(0.0); motor_cmd.tau(0.0)  # defaults
    if i == 30:          # Neck Pitch
        motor_cmd.q(0.3); motor_cmd.kp(5); motor_cmd.kd(0.1)
    elif i == 29:
        motor_cmd.kp(2);  motor_cmd.kd(0.05)
    else:
        motor_cmd.kp(0.0); motor_cmd.kd(0.0)

# kinematic_modes is a 5-element array by parallel-link group (waist/L_ankle/R_ankle/L_wrist/R_wrist).
# To change it, pass the full 5-element list to the setter (editing the list the getter returns has no effect).
low_cmd_msg.kinematic_modes([igc_sdk.KINEMATIC_MODE_PJS] * 5)
lowcmd_publisher.write(low_cmd_msg)

The full example can be found in the GitHub repository.