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:
| Middleware | Topic |
|---|---|
| ROS 2 | /mission/joystick_data |
| CycloneDDS | rt/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.
-1.0 on Axis 1 triggers maximum forward movement.| Axis Index | Input Range | Description |
|---|---|---|
| 0 | -1.0 to 1.0 | Lateral movement (Left/Right) |
| 1 | -1.0 to 1.0 | Longitudinal movement (Forward - / Backward +) |
| 3 | -1.0 to 1.0 | Rotational movement (Yaw about the Z-axis) |
| 4 | -1.0 to 1.0 | Postural adjustment (Body Height) |
| 5 | 0.0 to 1.0 | Gait 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 Index | Target Robot State | Description |
|---|---|---|
| 0 | SLEEP | Robot's home position |
| 1 | FIXED STAND | Locks joints to maintain a rigid, default standing posture. |
| 2 | MOVE | Engages the dynamic gait engine, allowing for velocity tracking. |
| 3 | FREESTAND | Enables 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_STANDstate 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_STANDto 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.
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 positionsdq_act,dq_ref→ Actual (measured) and Reference (target) joint velocitiesk_p→ Proportional joint stiffness gaink_d→ Derivative joint damping gain
2. Communication Channels
The hardware layer subscribes to structured joint command packets on the following network endpoints:
| Middleware | Topic |
|---|---|
| ROS 2 | /m2_metal/hw/joint_command |
| CycloneDDS | rt/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
| Parameter | Type | Units | Description |
|---|---|---|---|
q | float32[12] | rad | Target angular position for the joint (q_ref). |
dq | float32[12] | rad/s | Target angular velocity for the joint (dq_ref). |
kp | float32[12] | N·m/rad | Stiffness gain. Multiplier for the position error. |
kd | float32[12] | N·m·s/rad | Damping gain. Multiplier for the velocity error. |
tau | float32[12] | N·m | Direct feed-forward torque injection (τ_ff). |
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;
}