Force & Tactile Messages

HORUS provides message types for force/torque sensors, tactile arrays, impedance control, and haptic feedback systems commonly used in manipulation tasks.

All force types — WrenchStamped, TactileArray, ImpedanceParameters, ForceCommand, ContactInfo, ContactState, HapticFeedback — are re-exported through horus::prelude; use horus::prelude::*; is all you need.

WrenchStamped

6-DOF force and torque measurement from force/torque sensors. Implements PodMessage for zero-copy transfer.

use horus::prelude::*;

// Create wrench measurement
let force = Vector3::new(10.0, 5.0, -2.0);   // Newtons
let torque = Vector3::new(0.1, 0.2, 0.05);   // Newton-meters

let wrench = WrenchStamped::new(force, torque)
    .with_frame_id("tool0");

// Check magnitudes
println!("Force magnitude: {:.2} N", wrench.force_magnitude());
println!("Torque magnitude: {:.3} Nm", wrench.torque_magnitude());

// Safety check
let max_force = 50.0;   // N
let max_torque = 5.0;   // Nm
if wrench.exceeds_limits(max_force, max_torque) {
    println!("Safety limits exceeded!");
}

// Create from force only
let force_only = WrenchStamped::force_only(Vector3::new(0.0, 0.0, -10.0));

// Create from torque only
let torque_only = WrenchStamped::torque_only(Vector3::new(0.0, 0.0, 0.5));

// Low-pass filter noisy sensor readings against the previous sample
let previous_wrench = WrenchStamped::new(Vector3::new(9.8, 4.9, -2.1), torque);
let mut current = wrench;
current.filter(&previous_wrench, 0.1);  // alpha = 0.1 (heavy filtering)

Fields:

FieldTypeDescription
forceVector3Force [fx, fy, fz] in Newtons
torqueVector3Torque [tx, ty, tz] in Nm
point_of_applicationPoint3Force application point
frame_id[u8; 32]Reference frame
timestamp_nsu64Nanoseconds since epoch

Methods:

MethodDescription
new(force, torque)Create a new wrench measurement
force_only(force)Create from force only (zero torque)
torque_only(torque)Create from torque only (zero force)
with_frame_id(frame_id)Set reference frame (builder pattern)
force_magnitude()Get force vector magnitude
torque_magnitude()Get torque vector magnitude
exceeds_limits(max_force, max_torque)Check if limits exceeded
filter(&prev_wrench, alpha)Apply low-pass filter (alpha 0.0-1.0)

TactileArray

Pressure/force array from tactile sensors (fingertip sensors, skin patches). Serde-serialized message (not POD — it holds a Vec<f32>, so it uses the serialization path rather than zero-copy shared memory).

use horus::prelude::*;

// Create 8x8 grid tactile array
let mut tactile = TactileArray::new(8, 8);  // 8 rows x 8 cols
tactile.physical_size = [0.016, 0.016];  // 16mm x 16mm pad

// Set and get taxel readings
tactile.set_force(0, 0, 1.5);
tactile.set_force(0, 1, 2.0);
if let Some(reading) = tactile.get_force(0, 0) {
    println!("Taxel (0,0): {:.1} N", reading);
}

// Get raw taxel readings
let readings = &tactile.forces;  // row-major, len = rows * cols

// Total contact force
let total = tactile.total_force;  // [fx, fy, fz] in Newtons
println!("Total force: [{:.2}, {:.2}, {:.2}] N", total[0], total[1], total[2]);

// Detect contact
if tactile.in_contact {
    println!("Contact detected!");
}

// Center of pressure
let cop = tactile.center_of_pressure;  // [row, col] normalized 0.0-1.0
println!("Center of pressure: ({:.2}, {:.2})", cop[0], cop[1]);

// Get contact pattern
let pattern: Vec<bool> = tactile.forces.iter().map(|f| *f > 0.5).collect();

Fields:

FieldTypeDescription
rowsu32Number of taxel rows
colsu32Number of taxel columns
forcesVec<f32>Taxel force readings in Newtons (row-major, len = rows × cols)
total_force[f32; 3]Total contact force [fx, fy, fz] in Newtons
center_of_pressure[f32; 2]Center of pressure, normalized 0.0-1.0 (row, col)
in_contactboolWhether any contact is detected
physical_size[f32; 2]Physical sensor size [width_m, height_m]
timestamp_nsu64Nanoseconds since epoch

Methods:

MethodDescription
new(rows, cols)Allocate a rows × cols taxel grid
get_force(row, col)Get taxel reading as Option<f32> (None if out of bounds)
set_force(row, col, force)Set taxel reading (silently ignored if out of bounds)

ImpedanceParameters

Impedance control parameters for compliant manipulation. Implements PodMessage for zero-copy transfer.

use horus::prelude::*;

// Default impedance (moderate compliance)
let mut impedance = ImpedanceParameters::new();

// Compliant mode (low stiffness - for delicate tasks)
let compliant = ImpedanceParameters::compliant();
// stiffness: [100, 100, 100, 10, 10, 10]
// damping: [20, 20, 20, 2, 2, 2]

// Stiff mode (high stiffness - for precision tasks)
let stiff = ImpedanceParameters::stiff();
// stiffness: [5000, 5000, 5000, 500, 500, 500]
// damping: [100, 100, 100, 10, 10, 10]

// Enable/disable
impedance.enable();
impedance.disable();

// Custom parameters
impedance.stiffness = [500.0, 500.0, 200.0, 50.0, 50.0, 50.0];  // [Kx, Ky, Kz, Krx, Kry, Krz]
impedance.damping = [30.0, 30.0, 20.0, 3.0, 3.0, 3.0];          // [Dx, Dy, Dz, Drx, Dry, Drz]
impedance.force_limits = [50.0, 50.0, 30.0, 5.0, 5.0, 5.0];     // Safety limits

Fields:

FieldTypeDescription
stiffness[f64; 6]Stiffness [Kx, Ky, Kz, Krx, Kry, Krz]
damping[f64; 6]Damping [Dx, Dy, Dz, Drx, Dry, Drz]
inertia[f64; 6]Virtual inertia
force_limits[f64; 6]Force/torque limits
enabledu8Impedance control active (0 = off, 1 = on)
timestamp_nsu64Nanoseconds since epoch

Methods:

MethodDescription
new()Create with default moderate stiffness/damping
compliant()Create with low stiffness for delicate tasks
stiff()Create with high stiffness for precision tasks
enable()Enable impedance control
disable()Disable impedance control

ForceCommand

Hybrid force/position control command. Implements PodMessage for zero-copy transfer.

use horus::prelude::*;

// Pure force command
let force_cmd = ForceCommand::force_only(Vector3::new(0.0, 0.0, -5.0));  // 5N downward

// Hybrid force/position control
// Force control on Z axis, position control on X/Y
let force_axes: [u8; 6] = [0, 0, 1, 0, 0, 0];  // 1=force, 0=position per axis
let target_force = Vector3::new(0.0, 0.0, -10.0);
let target_position = Vector3::new(0.5, 0.3, 0.0);

let hybrid_cmd = ForceCommand::hybrid(force_axes, target_force, target_position);

// Surface contact following
let surface_normal = Vector3::new(0.0, 0.0, 1.0);  // Horizontal surface
let contact_force = 5.0;  // 5N contact force
let surface_cmd = ForceCommand::surface_contact(contact_force, surface_normal);

// Set timeout
let cmd_with_timeout = force_cmd.with_timeout(5.0);  // 5 second timeout

Fields:

FieldTypeDescription
target_forceVector3Desired force [N]
target_torqueVector3Desired torque [Nm]
force_mode[u8; 6]1 = force control, 0 = position control per axis
position_setpointVector3Position target for position-controlled axes
orientation_setpointVector3Orientation target (Euler angles)
max_deviationVector3Maximum position deviation
gains[f64; 6]Control gains
timeout_secondsf64Command timeout (0 = no timeout)
frame_id[u8; 32]Reference frame
timestamp_nsu64Nanoseconds since epoch

Methods:

MethodDescription
force_only(target_force)Create pure force command (force_mode = [1, 1, 1, 0, 0, 0] — the three translational axes are force-controlled)
hybrid(force_axes, target_force, target_position)Create hybrid force/position command
surface_contact(normal_force, surface_normal)Create surface following command
with_timeout(seconds)Set command timeout (builder pattern)

ContactInfo

Contact detection and classification. Implements PodMessage for zero-copy transfer.

use horus::prelude::*;

// Create contact info
let contact = ContactInfo::new(ContactState::StableContact, 15.0);  // 15N force

// Check contact state
if contact.is_in_contact() {
    println!("Contact force: {:.1} N", contact.contact_force);
    println!("Duration: {:.2}s", contact.contact_duration_seconds());
    println!("Confidence: {:.0}%", contact.confidence * 100.0);
}

ContactState values:

StateValueDescription
NoContact0No contact detected (default)
InitialContact1First contact moment
StableContact2Established contact
ContactLoss3Contact being broken
Sliding4Sliding contact
Impact5Impact detected

Fields:

FieldTypeDescription
stateu8Contact state (use ContactState as u8 to set)
contact_forcef64Contact force magnitude
contact_normalVector3Contact normal vector (estimated)
contact_pointPoint3Contact point (estimated)
stiffnessf64Contact stiffness (estimated)
dampingf64Contact damping (estimated)
confidencef32Detection confidence (0.0 to 1.0)
contact_start_timeu64Time contact was first detected (ns)
frame_id[u8; 32]Reference frame
timestamp_nsu64Nanoseconds since epoch

Methods:

MethodDescription
new(state, force_magnitude)Create new contact info (takes ContactState, stores as u8)
is_in_contact()Check if currently in contact (InitialContact, StableContact, or Sliding)
contact_duration_seconds()Get contact duration in seconds

HapticFeedback

Haptic feedback commands for user interfaces. Implements PodMessage for zero-copy transfer.

use horus::prelude::*;

// Vibration feedback
let vibration = HapticFeedback::vibration(
    0.8,   // intensity (0-1)
    250.0, // frequency (Hz)
    0.5    // duration (seconds)
);

// Force feedback
let force = HapticFeedback::force(
    Vector3::new(1.0, 0.0, 0.0),  // Force direction
    1.0                           // Duration (seconds)
);

// Pulse pattern
let pulse = HapticFeedback::pulse(
    0.6,   // intensity
    100.0, // frequency
    0.3    // duration
);

Pattern Types:

ConstantValueDescription
PATTERN_CONSTANT0Constant intensity
PATTERN_PULSE1Pulsing pattern
PATTERN_RAMP2Ramping intensity

Fields:

FieldTypeDescription
vibration_intensityf32Vibration intensity (0.0 to 1.0)
vibration_frequencyf32Vibration frequency in Hz
duration_secondsf32Duration of feedback in seconds
force_feedbackVector3Force feedback vector
pattern_typeu8Feedback pattern (see constants)
enabledu8Enable/disable feedback (0 = off, 1 = on)
timestamp_nsu64Nanoseconds since epoch

Methods:

MethodDescription
vibration(intensity, frequency, duration)Create vibration feedback (intensity clamped to 0-1)
force(force, duration)Create force feedback
pulse(intensity, frequency, duration)Create pulse pattern feedback

Force Control Node Example

use horus::prelude::*;

struct ForceControlNode {
    wrench_sub: Topic<WrenchStamped>,
    cmd_pub: Topic<ForceCommand>,
    impedance_pub: Topic<ImpedanceParameters>,
    target_force: f64,
    prev_wrench: Option<WrenchStamped>,
}

impl Node for ForceControlNode {
    fn name(&self) -> &str { "ForceControl" }

    fn tick(&mut self) {
        if let Some(mut wrench) = self.wrench_sub.recv() {
            // Apply low-pass filter
            if let Some(prev) = &self.prev_wrench {
                wrench.filter(prev, 0.2);
            }
            self.prev_wrench = Some(wrench);

            // Safety check
            if wrench.exceeds_limits(100.0, 10.0) {
                // Switch to compliant mode
                let compliant = ImpedanceParameters::compliant();
                self.impedance_pub.send(compliant);
                return;
            }

            // Force control to maintain target contact force
            let error = self.target_force - wrench.force.z;
            let correction = error * 0.001;  // Simple P control

            let cmd = ForceCommand::force_only(
                Vector3::new(0.0, 0.0, self.target_force + correction)
            );
            self.cmd_pub.send(cmd);
        }
    }
}

See Also