Native SDK Development •3. Source Code Analysis

3. Source Code Analysis

Updated: October 12, 2026

1 Directory Structure

PLAIN
native_sdk/
├── CMakeLists.txt                    # Top-level build entry (auto-detects core precompiled package)
├── build.sh / run.sh / clear.sh      # Build, run, and cleanup scripts
├── install.sh                        # Real-robot installation script
│
├── src/                              # Main modifiable source code
│   ├── executor/main.cc              # Program entry point
│   ├── hardware/                     # Gamepad driver example
│   ├── protocol/                     # ROS2 msg example
│   ├── ros2_node/                    # ROS2 message send/receive example
│   ├── runner/                       # Custom/example runner
│   │   ├── passive/                  # Example of motor continuously outputting low impedance
│   │   ├── pd_stand/                 # Example of using PD controller to maintain standing
│   │   ├── input_command_arbiter/    # Input source processing example
│   │   ├── ros2_bridge/              # ROS2 data exchange
│   │   ├── rl_dance_example/         # Dancing example based on RL approach
│   │   └── rl_walking_example/       # Walking example based on RL approach
│   └── data/_param/                  # Runner parameter parsing class
│
├── assets/
│   ├── config/<robot>/               # Configuration system entry (mode + task + runner parameters)
│   └── resource/                     # Resources such as urdf/xml/mesh/policy models
│
├── core/                             # Precompiled core library (do not modify directly)
│   ├── include/                      # Visible header files (interfaces)
│   ├── lib/                          # Precompiled so
│   └── registry/auto_register_runners.cc
│
├── cmake/                            # CMake helper scripts such as auto-registration
├── simulation/mujoco/                # Mujoco simulation project
└── scripts/                          # Helper scripts (run_robot/run_mujoco, etc.)

2 Startup Process

test\.jpg

3 Configuration Loading Chain

assets/config/<robot>/mode.yaml is the entry point of the configuration system, for example:

YAML
active_mode: sim
mode:
  sim:
    - tag: motion_task
      scope: task_motion/default
    - tag: pd_stand
      scope: pd_stand/default
  robot:
    - tag: motion_task
      scope: task_motion/default
    - tag: pd_stand
      scope: pd_stand/robot

The loading logic can be understood as:

  1. First select sim/robot based on active_mode
  2. Establish the tag -> scope mapping (e.g. pd_stand -> pd_stand/default)
  3. BasicParam(tag) finds scope based on tag
  4. Use LOAD_PARAM(xxx) in the parameter class to read fields from the corresponding YAML

test\.jpg

4 Task and Runner

4.1 Concepts and Relationships

  • Task
    • It is a scheduling unit that defines threads, periods, priorities, and which Runners to execute (runner[]).
    • There are two types of Task in configuration:
      • task_resident/*.yaml: resident tasks, run in parallel over the long term after startup.
      • task_motion/*.yaml: motion tasks, only one motion task is active at a time, switched via keys or automatic transition.
  • Runner
    • It is an algorithm execution unit; each Runner is responsible for a specific piece of logic (e.g. estimation, control, actuation).
  • The relationship can be understood as: Task(scheduling shell) -> Runner(business logic); Task drives the Runner lifecycle interfaces periodically.

4.2 Runner Lifecycle and State Transitions

  • BasicRunner (core/include/basic/basic_runner.h) defines a unified lifecycle:
    • Initialize(): initialization after construction (resources, default state)
    • Enter(): called when entering the current task (parameter loading, initial value preparation)
    • Run(): periodically executes the main logic
    • TryExit(): exit attempt stage (can execute reset/convergence logic over multiple cycles)
    • Exit(): performs one-time cleanup after confirming exit (variable reset/initialization preparation)
    • End(): cleanup at the end of the lifecycle
  • MotionRunner (core/include/basic/motion_runner.h) adds control-related interfaces on top of the above lifecycle:
    • SetupContext() / TeardownContext(): set and restore the global context when entering/exiting
    • GetMutableOutput().SetCommand(...): write to the control output cache
    • GetOutput(): read the current output cache
  • Typical call sequence (Initialize() is only called when the runner is constructed):
    • Runner of resident task: Enter -> Run -> TryExit -> Exit -> End
    • Runner of motion task: SetupContext -> Enter -> Run -> TryExit -> Exit -> TeardownContext -> End
  • TryExit() vs Exit():
    • TryExit(): tends toward continuous reset, suitable for "needs time to complete" exit processes (e.g. planning the arm back to origin); may be called in a loop, and reports completion via kTrying/kCompleted.
    • Exit(): tends toward one-time processing, usually performs variable reset, state cleanup, and pre-switch cleanup.
  • Common states and transition semantics:
    • RunnerState: kRunning / kTryExit / kFault
    • TransitionState: kCompleted / kTrying / kFailed / kRestoreRunning
  • The state machine can be understood as follows:
    • Running stage: kRunning (continuously calls Run())
    • Trigger exit: kTryExit (enters TryExit())
    • Exit result:
      • kCompleted: executes Exit() and completes the transition
      • kTrying: remains in the TryExit() stage
      • kRestoreRunning: returns to kRunning
      • kFailed: enters fault/failure handling (usually taken over by the upper layer)

5 RL Walking Policy Deployment Code Analysis

This section takes RlWalkingExampleRunner as an example to provide an in-depth analysis of how the RL-based walking policy is deployed in the real-robot framework. This Runner inherits from MotionRunner and strictly follows the execution paradigm of "data acquisition - logic computation - cached output". The overall deployment logic can be broken down into the following three core modules: global parameter configuration, system input source acquisition, and system output with cache arbitration.

5.1 Global Parameter Configuration and Model Initialization

Before entering periodic control, the system needs to complete the configuration of low-level mechanism switches, the dynamic loading of configuration parameters, and the instantiation of the neural network model.

  • **Global control context setup (SetupContext)**For RL policies trained with serial joints, you need to manually set parallel_by_classic_parser to False
C++
void RlWalkingExampleRunner::SetupContext() {
data_store_->parallel_by_classic_parser.store(false);
}

Ankle joint control

test\.jpg

test\.jpg

For non-ankle joint control modes, the MIT control mode is used. For ankle joint control modes, there are currently two control modes:

  1. Serial-joint MIT mode commands to parallel-joint MIT mode commands: you need to specify parallel_by_classic_parser=True (e.g., PD standing mode). The user only needs to provide the position, velocity, and feedforward torque commands for the serial joints, which are automatically converted into the motor's position, velocity, and feedforward torque commands. Kp and Kd retain the serial-joint values and are not converted;
  2. Serial-joint torque mode commands to parallel-joint torque mode commands: you need to specify parallel_by_classic_parser=False (e.g., RL walking mode). The user only needs to provide the position, velocity, and feedforward torque commands for the serial joints. The total torque command of the serial joints is automatically converted into the motor's torque command, and the Kp and Kd sent to the motors are set to 0.

Note:

  1. parallel_by_classic_parser defaults to True. For an RL policy trained with serial joints, you need to manually set parallel_by_classic_parser to False;
  2. Currently, the ankle joint only supports the above two control modes for converting serial-joint commands to parallel-joint commands, and does not support sending commands directly to the ankle motor layer (if you implement the serial-parallel resolution yourself, please contact us).
  • Parameter file reading and dynamic loadingWhen the Runner is constructed, the default configuration is obtained via ParamManager. When the state machine triggers a task switch and enters the Enter() stage, the system validates param_tag_. This design allows the robot to dynamically reload configuration files of different tags (e.g. flat-ground walking, stair climbing, and other gait parameters) without stopping, achieving highly flexible scheduling.
C++
if (!param_tag_.empty() && param_tag_ != last_param_tag_) {
  param_ = data::ParamManager::create<data::RlWalkingExampleParam>(param_tag_);
  last_param_tag_ = param_tag_;
}
  • Inference engine and model loadingThe system uses math::MNNModel as a lightweight neural network inference engine. By combining the global environment path with the relative path specified in the parameter file, it precisely locates and loads the .mnn model file.
C++
mlp_net_ = std::make_unique<math::MNNModel>(
  common::PathJoin(common::GlobalPathManager::GetInstance().GetConfigPath(), 
  param_->policy_file)
);

5.2 System Input and Proprioception

During the periodic execution of Run(), the Runner needs to extract the robot's physical state (proprioception) and external interaction commands at high frequency from the global data pool (data_store_) to build the observation space for the network.

  • **Joint state information (Joint State)**Accurately obtain the actual position q_real_ and actual velocity qd_real_ of each joint. The underlying architecture supports obtaining more dimensions of state (such as feedforward torque, stiffness, damping, etc.) via the JointInfoType enum class, meeting the needs of different policies.
C++
// Extract the current joint's true position and velocity
data_store_->joint_info.GetState(data::JointInfoType::kPosition, q_real_);
data_store_->joint_info.GetState(data::JointInfoType::kVelocity, qd_real_);
  • **Inertial measurement unit information (IMU State)**Obtain the body quaternion and angular velocity. To eliminate installation errors and unify the data into the robot base coordinate system, the code performs standard coordinate system rotation transformations and computes the gravity projection vector used to represent the body posture.
C++
// Parse installation bias and local quaternion
Eigen::Matrix3d R_install = math::RotationMatrixd(math::RollPitchYawd(imu_install_bias_)).matrix();
Eigen::Matrix3d R_local = math::RotationMatrixd(data_store_->imu_info.Get()->quaternion).matrix();
Eigen::Matrix3d R_real = R_local * R_install.transpose();

// Compute angular velocity and gravity projection in base frame
Eigen::Vector3d w_real = R_real.transpose() * R_local * data_store_->imu_info.Get()->angular_velocity;
Eigen::Vector3d euler_xyz = math::RollPitchYawd(math::RotationMatrixd(R_real)).vector();
Eigen::Vector3d projected_gravity_real = -R_real.transpose() * Eigen::Vector3d::UnitZ();
  • **User remote command (Remote Command)**Parse the gamepad input data and convert it into desired linear and angular velocities combined with scaling factors of different dimensions. To prevent sudden command changes from impacting the model, the system introduces a first-order low-pass filter (lpf_command_) for signal smoothing.
C++
void RlWalkingExampleRunner::UpdateRemoteCommand() {
  const auto& gamepad = data_store_->gamepad_info.Get();
  // Compute desired XYZ-axis velocity based on stick deflection and scaling parameters
  command_.x() = (gamepad->LeftStick_X >= 0) ? gamepad->LeftStick_X * param_->command_scale_pos.x()
                                             : gamepad->LeftStick_X * param_->command_scale_neg.x();
  /* ... y, z axis handled similarly ... */

  // Apply low-pass filter to smooth command input
  if (param_->enable_remote_command_lpf) {
    command_ = lpf_command_->Update(command_);
  }
}

5.3 Command Cache and Global Arbitration Dispatch

The actions computed by the algorithm cannot directly pass through to the hardware; they must follow the framework's caching and unified scheduling mechanism to ensure the safety and strict timing of robot control commands.

  • Write to local output cacheAfter completing forward inference, the Runner uses the GetMutableOutput().SetCommand(...) interface to write the desired position (q_des_), desired velocity (qd_des_), feedforward torque (tau_ff_des_) and the underlying PD gains into its own dedicated cache.
C++
void RlWalkingExampleRunner::SendMotorCommand() {
  // In pure position control mode, explicitly set desired velocity and feedforward torque to zero
  qd_des_ = Eigen::VectorXd::Zero(model_param_->num_total_joints);
  tau_ff_des_ = Eigen::VectorXd::Zero(model_param_->num_total_joints);

  // Only write the command to this Runner's local cache
  GetMutableOutput().SetCommand(q_des_, qd_des_, joint_kp_, joint_kd_, tau_ff_des_);
}
  • Task-level integration and synchronized dispatchThe core of this architecture design is data bus decoupling. The Task Manager plays the role of an "arbiter" at the end of each control cycle. It uniformly collects the local caches of all active Runners, processes possible command interpolation (smooth transition), singularity checks, and torque limits. After confirming safety, it strictly synchronizes the packaged complete data frame to each underlying motor according to the bus communication cycle.

6 Control Input and ROS2 Integration

Native SDK provides a unified input and external data interface mechanism for receiving control commands and exchanging data with external systems. Overall, it can be divided into two categories:

6.1 1) Control Input

The control input of Native SDK is uniformly handled by runner/input_command_arbiter.

Different input devices must first be wrapped as corresponding Adapters, and then registered into `InputCommandArbiterRunner` to participate in input source management.

The current input source registration location is as follows:

C++
InputCommandArbiterRunner::InputCommandArbiterRunner(
    std::string_view name,
    const std::shared_ptr<data::DataStore>& data_store)
    : BasicRunner(name, data_store) {
  // Register input sources from low to high priority.
  RegisterInputSource("gamepad", std::make_shared<GamepadInputAdapter>("gamepad", data_store));
  RegisterInputSource("virtual_gamepad", std::make_shared<VirtualGamepadInputAdapter>("virtual_gamepad", data_store));
}

Two types of input sources are currently built in:

  • gamepad: physical gamepad input
  • virtual_gamepad: virtual gamepad input

To extend a new input method (such as keyboard, network control, custom remote control, etc.), the following steps are usually required:

  1. Add the corresponding Input Adapter
    • Responsible for converting external input into the framework's internal unified control input format
  2. Register the Adapter in InputCommandArbiterRunner
    • Access the input source arbitration flow via RegisterInputSource(...)
  3. Set priority and switching strategy as needed
    • Input sources participate in priority management according to registration order (the current convention in code comments is from low to high priority)

This allows the new input device to be integrated into the existing system without modifying the upper-level control logic.

6.2 2) ROS2 Integration

For Native SDK, ROS2 is an independent plug-in communication module that bridges ROS2 data with Native SDK via ros2_bridge.

To extend ROS2 functionality, the following types of modifications are usually involved:

  • Add a new ROS2 Node
    • Add a new node implementation in src/ros2_node
  • Add Topic
    • Add the corresponding publish / subscribe topic in the existing node
  • Add Msg
    • Add the corresponding msg definition in src/protocol

After adding the new code, it also needs to be explicitly activated in the ROS2 Bridge configuration.

Example of configuration file location:

Bash
assets/<robot>/ros2_bridge/default.yaml

Example configuration is as follows:

YAML
activated_node:
  - hardware_interface_node

manager_node:
  subscribe_topics:
    node_control: /motion/node_control

hardware_interface_node:
  enable: true
  period: 0.002
  mapping_periods:
    joint_state: 0.002
    gamepad_keys: 0.02
    motor_debug: 0.01
    power_info: 0.05
  publish_topics:
    imu_info: /hardware/imu_info
    gamepad_keys: /hardware/gamepad_keys
    motor_debug: /hardware/motor_debug
    power_info: /hardware/power_info
    motor_state: /hardware/motor_state
    motor_command: /hardware/motor_command
    joint_state: /hardware/joint_state
    joint_command_feedback: /hardware/joint_command_feedback
  subscribe_topics:
    led_control: /hardware/led_control

The extension rules are as follows:

  • If adding a node
    • You need to add the corresponding node name in activated_node
    • Also supplement the node's configuration items
  • If adding a topic
    • You need to add the topic name and mapping relationship under publish_topics or subscribe_topics in the corresponding node configuration
  • If adding a msg
    • You first need to add the message definition in src/protocol, then implement the message send/receive logic and configuration mapping in the node
3. Source Code Analysis | EngineAI Open Platform