Sensor Interface

Sensor Interface provides telemetry required by developers and researchers to build custom applications and advanced control algorithms. This interface streams high-frequency diagnostic and kinematic data directly from the Svan M2's hardware. This includes joint kinematics, estimated torques, Inertial Measurement Unit (IMU) readings, and power consumption statistics.

1. Communication Channels

The hardware layer publishes structured sensor data packets on the following network endpoints:

MiddlewareTopic
ROS 2/m2_metal/hw/sensor_data
CycloneDDSrt/m2_metal/hw/sensor_data

2. SensorData Message Structure

The sensor data payload utilizes the xterra/SensorData message. This data is logically divided into centralized power diagnostics, individual actuator kinematics (arrays of size 12), and centralized IMU readings.

Raw Message Definition

xterra/msg/SensorData.msg

uint32    driver_fault
float32   driver_voltage
float32   driver_power
float32   fet_temp_c
float32[12]  q
float32[12]  dq
float32[12]  ddq
float32[12]  tau_est
float32[12]  q_current
float32[12]  d_current
float32[4]   quat
float32[3]   gyro
float32[3]   accel
float32[3]   rpy

2.1 Power & Diagnostics

These fields provide system-level health and electrical monitoring for the robot's power distribution and motor driving hardware.

ParameterTypeUnitsDescription
driver_faultuint32N/AHardware fault code. A value of 0 indicates nominal operation.
driver_voltagefloat32Volts (V)Main bus voltage supplied to the motor drivers.
driver_powerfloat32Watts (W)Total instantaneous power consumption of the actuator network.

2.2 Actuator Kinematics & Dynamics ([12] Arrays)

These arrays contain the mechanical and electrical state of the robot's 12 independent joints. The index 0-11 corresponds to specific leg joints (e.g., Abduction, Hip, Knee).

ParameterTypeUnitsDescription
qfloat32[12]radMeasured angular position of the joint.
dqfloat32[12]rad/sMeasured angular velocity of the joint.
ddqfloat32[12]rad/s²Estimated angular acceleration of the joint.
tau_estfloat32[12]N·mEstimated output torque at the joint (derived from current).
q_currentfloat32[12]Amperes (A)Quadrature-axis (torque-producing) current from the FOC driver.

2.3 Body IMU Data

These arrays provide the spatial orientation and acceleration of the robot's base, essential for balancing and state estimation.

ParameterTypeUnitsDescription
quatfloat32[4]unitlessBody orientation represented as a quaternion [x, y, z, w].
gyrofloat32[3]rad/sAngular velocity of the base link [roll_rate, pitch_rate, yaw_rate].
accelfloat32[3]m/s²Linear acceleration of the base link [x, y, z].
rpyfloat32[3]radBody orientation represented as Euler angles [roll, pitch, yaw].

3. Implementation Examples

The following examples demonstrate how to construct subscriber nodes to ingest and parse the SensorData payload across the supported middlewares.

3.1 Sensor Subscriber (ROS 2)

  • Objective: Parse nested actuator arrays and IMU data using ROS 2 subscriber node.
  • Use Case: High-level observation, data logging (rosbags), or UI visualization (e.g., RViz).
  • Key Highlights:
    • Accesses data via standard ROS 2 shared pointers.
    • Utilizes RCLCPP_INFO_THROTTLE to maintain high-frequency underlying data ingestion while keeping the debug console readable.
#include <functional>
#include "rclcpp/rclcpp.hpp"
#include "xterra/msg/sensor_data.hpp"

class SensorDataNode : public rclcpp::Node
{
public:
    SensorDataNode() : Node("sensor_data_subscriber")
    {
        // Subscriber for sensor data with attached callback
        sub_ = this->create_subscription<xterra::msg::SensorData>(
            "/m2_metal/hw/sensor_data", 10,
            std::bind(&SensorDataNode::sensor_callback, this,
                      std::placeholders::_1));
    }

private:
    void sensor_callback(const xterra::msg::SensorData::SharedPtr msg)
    {
        // Access Front-Left Abduction joint data (Index 0)
        float joint_pos    = msg->q[0];
        float joint_vel    = msg->dq[0];
        float joint_torque = msg->tau_est[0];

        // Access IMU quaternion
        float quat_x = msg->quat[0];
        float quat_y = msg->quat[1];
        float quat_z = msg->quat[2];
        float quat_w = msg->quat[3];

        // Log data (throttled every 500ms to keep console readable)
        RCLCPP_INFO_THROTTLE(this->get_logger(), *this->get_clock(), 500,
            "Joint[0] pos: %.3f, vel: %.3f, tau: %.3f | "
            "Quat: [%.2f,%.2f,%.2f,%.2f]",
            joint_pos, joint_vel, joint_torque,
            quat_x, quat_y, quat_z, quat_w);
    }

    rclcpp::Subscription<xterra::msg::SensorData>::SharedPtr sub_;
};

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

3.2 Sensor Subscriber (CycloneDDS)

  • Objective: Read real-time joint states and IMU sensor data from the hardware layer using CycloneDDS.
  • Use Case: High-frequency, custom controllers where minimizing read latency is critical.
  • Key Highlights:
    • Bypasses ROS 2 abstraction layer, minimizing network latency.
    • Registers a direct callback with the DDS middleware, allowing instantaneous access to data arrays as soon as network packets arrive.
#include <iostream>
#include <functional>
#include <thread>
#include <chrono>
// Include header for generated sensor data
#include "SensorData.hpp"
// Include custom header for Raw CycloneDDS subscriber abstraction
#include "dds_subscriber.hpp"

// Define the DDS Domain ID (default is 0)
const uint32_t DOMAIN_ID = 0;

// Callback function triggered upon receiving new hardware sensor data
void sensorCallback(const xterra::msg::dds_::SensorData_& msg) {
    // Power diagnostics
    std::cout << "System Voltage: " << msg.driver_voltage() << " V\n";

    // Kinematics for Joint 0 (Front-Right Abduction)
    std::cout << "Joint[0] Position: " << msg.q()[0] << " rad\n";

    // Body IMU orientation
    std::cout << "Quaternion: ["
              << msg.quat()[0] << ", " << msg.quat()[1] << ", "
              << msg.quat()[2] << ", " << msg.quat()[3] << "]\n";
}

int main() {
    auto sub = std::make_shared<DDSSubscriber<xterra::msg::dds_::SensorData_>>(
        "rt/m2_metal/hw/sensor_data",
        std::bind(&sensorCallback, std::placeholders::_1),
        DOMAIN_ID);

    std::cout << "Listening on rt/m2_metal/hw/sensor_data..." << std::endl;

    // Block thread to keep subscriber alive
    while (true) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    return 0;
}