Control Interfaces

The Svan M2 provides two distinct control interfaces: a High-Level Control Interface for state management and velocity commands, and a Low-Level Control Interface for direct joint actuator access. Choose the interface that matches your application's requirements.

High-Level Control Interface

This document outlines the High-Level Control Interface, which serves as the primary gateway for state management and directional velocity control of the Svan M2 quadruped.

This interface is designed for both direct human-in-the-loop teleoperation and programmatic command generation by external autonomous navigation stacks.

1. Communication Channels

The locomotion controller listens for structured command packets on the following network endpoints:

MiddlewareTopic
ROS 2/mission/joystick_data
CycloneDDSrt/mission/joystick_data

2. JoyData Message Structure

The high-level command interface utilizes the xterra/JoyData message structure. This message contains priority scheduling, continuous velocity requests (axes), and discrete state transitions (buttons).

2.1 Analog Axes (float32[6] axes)

The axes array maps continuous normalized floats to spatial velocity and posture targets.

Note: Axes 0, 1, 3, and 4 utilize inverted logic. For example, a value of -1.0 on Axis 1 triggers maximum forward movement.
Axis IndexInput RangeDescription
0-1.0 to 1.0Lateral movement (Left/Right)
1-1.0 to 1.0Longitudinal movement (Forward - / Backward +)
3-1.0 to 1.0Rotational movement (Yaw about the Z-axis)
4-1.0 to 1.0Postural adjustment (Body Height)
50.0 to 1.0Gait parameter (Increases Step Height during locomotion if value is greater than 0.5 or vice versa)

2.2 Digital Buttons (uint8[12] buttons)

The buttons array triggers discrete state machine transitions within the robot's native controller. Setting an index to 1 requests a transition to the corresponding state.

Button IndexTarget Robot StateDescription
0SLEEPRobot's home position
1FIXED STANDLocks joints to maintain a rigid, default standing posture.
2MOVEEngages the dynamic gait engine, allowing for velocity tracking.
3FREESTANDEnables pose control while foot remaining stationary on ground.

3. Safety & Transition Logic

The high-level controller implements safety checks to prevent hardware damage during invalid state transitions.

  • Transitions exiting the FIXED_STAND state require the robot's base link to be near-level (Roll < 1.5 rad and Pitch < 1.5 rad). Commands issued while exceeding these limits are dynamically rejected.
  • Rollover Protection: If the base link exceeds critical inclination limits (Roll or Pitch > 1.5 rad) during active locomotion, the system automatically triggers an emergency reversion to FIXED_STAND to brace for impact and disable the dynamic gait engine.
  • Emergency Override: In the event of an emergency or unexpected behavior from external programmatic controllers, an operator can actuate the physical joystick to immediately override network commands and regain manual control of the system.

4. Implementation Examples

The following examples demonstrate how to construct and publish the JoyData command payload across the supported middleware. Both examples request a transition from SLEEP state to FIXEDSTAND state to the MOVE state and command a continuous forward drive.

4.1 Velocity Command Publisher (ROS 2)

  • Objective: Send state transitions and directional movement commands directly to the hardware via ROS2 node.
  • Use Case: Integration with autonomous navigation stacks or standard ROS 2 HID mapping nodes.
  • Key Highlights:
    • Utilizes a wall timer to publish at a steady 200Hz (5ms).
#include <chrono>
#include <functional>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "xterra_msgs/msg/joy_data.hpp"

using namespace std::chrono_literals;

class JoyDataPublisherNode : public rclcpp::Node
{
public:
    JoyDataPublisherNode() : Node("high_level_command_publisher")
    {
        // Publisher targeting the mission input topic (Queue size 10)
        pub_ = this->create_publisher<xterra_msgs::msg::JoyData>(
            "/mission/joystick_data", 10);

        // Record the exact time the node started
        start_time_ = this->now();

        // Setup a timer to continuously send the drive command every 5ms (200Hz)
        timer_ = this->create_wall_timer(
            5ms, std::bind(&JoyDataPublisherNode::timer_callback, this));
    }

private:
    void timer_callback()
    {
        auto msg = xterra_msgs::msg::JoyData();
        msg.priority = 100;  // Set priority to override default idle state

        // Calculate how much time has passed since the node started
        auto elapsed_time = this->now() - start_time_;

        if (elapsed_time.seconds() < 1.0) {
            // PHASE 1: For the first 1.0 second, transition from SLEEP to FIXED STAND
            msg.buttons[1] = 1;
            RCLCPP_INFO_THROTTLE(this->get_logger(), *this->get_clock(), 500,
                "State: FIXED STAND (Waiting...)");
        } else {
            // PHASE 2: After 1.0 second, transition to MOVE and drive forward
            msg.buttons[2] = 1;
            msg.axes[1] = -0.5f;  // Command forward drive (inverted logic)
            RCLCPP_INFO_THROTTLE(this->get_logger(), *this->get_clock(), 1000,
                "State: MOVE | Publishing forward drive command");
        }

        // Publish the command continuously at 200Hz to satisfy watchdogs
        pub_->publish(msg);
    }

    rclcpp::Publisher<xterra_msgs::msg::JoyData>::SharedPtr pub_;
    rclcpp::TimerBase::SharedPtr timer_;
    rclcpp::Time start_time_;
};

int main(int argc, char * argv[])
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<JoyDataPublisherNode>());
    rclcpp::shutdown();
    return 0;
}

4.2 Velocity Command Publisher (CycloneDDS)

  • Objective: Send state transitions and directional movement commands directly to the hardware via native DDS.
#include <iostream>
#include <memory>
// Include generated joystick data header
#include "JoyData.hpp"
// Include custom raw CycloneDDS publisher abstraction
#include "dds_publisher.hpp"

const uint32_t DOMAIN_ID = 0;

void sendMissionCommand() {
    auto pub = std::make_shared<DDSPublisher<xterra_msgs::msg::dds_::JoyData_>>(
        "rt/mission/joystick_data", DOMAIN_ID);

    xterra_msgs::msg::dds_::JoyData_ msg;

    // Set priority for this channel
    msg.priority() = 100;

    // Request transition to MOVE state (Button 2)
    msg.buttons()[2] = 1;

    // Command gradual forward drive (Axis 1, inverted logic)
    msg.axes()[1] = -0.5f;

    // NOTE: In production, call this inside a 200 Hz loop
    pub->publish(msg);
    std::cout << "Mission command published.\n";
}

Low-Level Control Interface

The Low-Level Control Interface bypasses the Svan M2 native locomotion controller, providing direct access to the 12 actuators.

This interface is strictly reserved for developers implementing custom control architectures where exact joint kinematics and dynamics is dictated by an external computational node.

WARNING: This interface disables ALL built-in locomotion, balancing, and safety controllers. The developer assumes complete responsibility for dynamic stability, joint limit enforcement, and actuator safety. Improper use can result in hardware damage or dangerous robot behavior.

1. Actuator Control Equation

The onboard motor drivers process incoming joint commands utilizing a combination of feed-forward torque injection and Proportional-Derivative (PD) error tracking. The final electrical torque command τ applied to each motor coil is computed onboard at high frequency as:

τ = τ_ff + k_p × (q_ref − q_act) + k_d × (dq_ref − dq_act)

Where:

  • τ → Final torque command applied to the motor
  • τ_ff → Feed-forward torque injection (provided by the controller)
  • q_act, q_ref → Actual (measured) and Reference (target) joint positions
  • dq_act, dq_ref → Actual (measured) and Reference (target) joint velocities
  • k_p → Proportional joint stiffness gain
  • k_d → Derivative joint damping gain

2. Communication Channels

The hardware layer subscribes to structured joint command packets on the following network endpoints:

MiddlewareTopic
ROS 2/m2_metal/hw/joint_command
CycloneDDSrt/m2_metal/hw/joint_command

3. JointData Message Structure

Because this interface communicates with 12 motors simultaneously, the communication node uses arrays of size 12. Indices 0-11 correspond directly to the kinematic mapping used in the SensorData arrays.

3.1 Actuator Target Parameters

ParameterTypeUnitsDescription
qfloat32[12]radTarget angular position for the joint (q_ref).
dqfloat32[12]rad/sTarget angular velocity for the joint (dq_ref).
kpfloat32[12]N·m/radStiffness gain. Multiplier for the position error.
kdfloat32[12]N·m·s/radDamping gain. Multiplier for the velocity error.
taufloat32[12]N·mDirect feed-forward torque injection (τ_ff).
Note: Pure torque control can be achieved by setting kp and kd to 0.0 and populating only the tau array. Conversely, pure position control can be achieved by setting tau to 0.0 and populating q, kp, and kd.

4. Implementation Examples

The motor driver updates joint command from the communication node at a rate of 1.5 kHz. Command packets must be streamed continuously (typically at 200Hz or higher is recommended). If the driver fails to receive a JointData packet within the predefined watchdog window, the motors will maintain their last commanded state (zero-order hold) until new data is received.

4.1 Joint Command Publisher (ROS 2)

  • Objective: Force all joints to maintain a set static position utilizing a PD tracking loop.
  • Use Case: Basic testing motor driver communication pathways, or initializing custom controllers.
  • Key Highlights:
    • Implements a strict 5ms (200Hz) publishing timer.
    • Make sure to keep the robot in sleep position.
    • Explicitly zeroes out feed-forward torque, relying entirely on stiffness and damping gains.
#include <chrono>
#include <functional>
#include "rclcpp/rclcpp.hpp"
#include "xterra/msg/joint_data.hpp"

using namespace std::chrono_literals;

class JointCommandNode : public rclcpp::Node
{
public:
    JointCommandNode() : Node("low_level_joint_publisher")
    {
        pub_ = this->create_publisher<xterra::msg::JointData>(
            "/m2_metal/hw/joint_command", 10);

        // Timer callback to publish commands at 200Hz (5ms)
        // High frequency is mandatory to prevent motor watchdog timeouts
        timer_ = this->create_wall_timer(
            5ms, std::bind(&JointCommandNode::publish_command, this));
    }

private:
    void publish_command()
    {
        auto cmd = xterra::msg::JointData();

        float target_pos[12] = {
             0.0001f,  1.306f, -2.738f,
            -0.0001f,  1.306f, -2.738f,
             0.0001f,  1.306f, -2.738f,
            -0.0001f,  1.306f, -2.738f
        };

        // Command all joints to hold the sleep pose
        for (int i = 0; i < 12; i++) {
            cmd.q[i]   = target_pos[i];  // Position target (rad)
            cmd.dq[i]  = 0.0f;           // Velocity target (rad/s)
            cmd.kp[i]  = 50.0f;          // P gain (Stiffness)
            cmd.kd[i]  = 1.0f;           // D gain (Damping)
            cmd.tau[i] = 0.0f;           // Torque feed-forward (N·m)
        }

        pub_->publish(cmd);
    }

    rclcpp::Publisher<xterra::msg::JointData>::SharedPtr pub_;
    rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char * argv[])
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<JointCommandNode>());
    rclcpp::shutdown();
    return 0;
}

4.2 Joint Command Publisher (CycloneDDS)

  • Objective: Stream explicit actuator targets directly to the hardware layer using native CycloneDDS.
  • Use Case: Serving as the output stage for custom high-frequency control algorithms.
  • Key Highlights:
    • Minimizes network latency by avoiding ROS 2 abstraction overhead.
    • Make sure to keep the robot in sleep position.
    • Demonstrates a standard synchronous while loop implementation typical of standalone C++ controller binaries.
#include <iostream>
#include <thread>
#include <chrono>
// Include header for generated joint data
#include "JointData.hpp"
// Include custom header for Raw CycloneDDS publisher abstraction
#include "dds_publisher.hpp"

// Define the DDS Domain ID
const uint32_t DOMAIN_ID = 0;

int main() {
    // Create the DDS publisher targeting the hardware layer
    auto pub = std::make_shared<xterra::DDSPublisher<xterra::msg::dds_::JointData_>>(
        "rt/m2_metal/hw/joint_command", DOMAIN_ID);

    std::cout << "Starting native DDS joint command stream at 200Hz..."
              << std::endl;

    // Instantiate the command message
    xterra::msg::dds_::JointData_ cmd;

    float target_pos[12] = {
         0.0001f,  1.306f, -2.738f,
        -0.0001f,  1.306f, -2.738f,
         0.0001f,  1.306f, -2.738f,
        -0.0001f,  1.306f, -2.738f
    };

    // 200Hz Control Loop (5ms period)
    const auto loop_period = std::chrono::milliseconds(5);

    while (true) {
        auto start_time = std::chrono::steady_clock::now();

        // Populate parameters for all 12 joints
        for (int i = 0; i < 12; i++) {
            cmd.q()[i]   = target_pos[i];  // Target position  (rad)
            cmd.dq()[i]  = 0.0f;           // Target velocity  (rad/s)
            cmd.kp()[i]  = 50.0f;          // Stiffness gain   (N·m/rad)
            cmd.kd()[i]  = 1.0f;           // Damping gain     (N·m·s/rad)
            cmd.tau()[i] = 0.0f;           // Feed-forward torque (N·m)
        }

        // Transmit synchronized arrays to the motor drivers
        pub->publish(cmd);

        // Enforce loop timing to satisfy hardware watchdog
        std::this_thread::sleep_until(start_time + loop_period);
    }
    return 0;
}