Force & Haptics Messages

Force messages handle the physical interaction between your robot and the world — measuring contact forces, commanding force-controlled actuators, adjusting compliance, and providing haptic feedback to operators. This is the most physics-intensive message category.

from horus import (
    WrenchStamped, ForceCommand, ImpedanceParameters,
    HapticFeedback, ContactInfo, TactileArray, Vector3,
)

WrenchStamped

A 6-DOF force/torque measurement from a force/torque sensor. "Wrench" is the physics term for combined force + torque.

Constructor

wrench = WrenchStamped(fx=10.0, fy=0.0, fz=-9.81, tx=0.0, ty=0.5, tz=0.0)

Forces in Newtons (N), torques in Newton-meters (Nm).

.force_only(vector) — Force Without Torque

from horus import Vector3
w = WrenchStamped.force_only(Vector3(x=0.0, y=0.0, z=-20.0))
# torque is zero

Creates a wrench with only force components. Use when modeling pure forces (gravity, push/pull) without any rotational component.

.torque_only(vector) — Torque Without Force

w = WrenchStamped.torque_only(Vector3(x=0.0, y=0.5, z=0.0))
# force is zero

.force_magnitude() — Total Force Strength

print(f"Force: {wrench.force_magnitude():.1f} N")

Euclidean magnitude of the force vector: sqrt(fx² + fy² + fz²). This is the total force regardless of direction — use it for safety checks ("is the force too high?") and monitoring.

Typical ranges by robot type:

  • Small arm (UR3): 0-30N normal, >50N = concerning
  • Large arm (UR10): 0-100N normal, >150N = concerning
  • Gripper: 0-50N grasp force

.torque_magnitude() — Total Torque Strength

print(f"Torque: {wrench.torque_magnitude():.2f} Nm")

.exceeds_limits(max_force, max_torque) — Safety Check

if wrench.exceeds_limits(max_force=50.0, max_torque=5.0):
    estop_topic.send(EmergencyStop.engage("Force limit exceeded"), node)
    cmd_topic.send(CmdVel.zero(), node)

Returns True if either force_magnitude() > max_force OR torque_magnitude() > max_torque. This is a safety-critical method — call it every tick when the robot is in contact with the environment or near humans.

Common mistake: Not calling this frequently enough. Force spikes happen in milliseconds — checking at 10Hz might miss a dangerous impact. Check at your control rate (typically 100-1000Hz).

.filter(prev_wrench, alpha) — Noise Smoothing

prev = wrench  # Save previous reading
new_reading = WrenchStamped(fx=10.5, fy=0.1, fz=-9.8, tx=0.0, ty=0.5, tz=0.0)
new_reading.filter(prev, alpha=0.8)
# Result: 80% new reading + 20% previous reading

Applies exponential moving average (EMA) filter in-place. alpha controls responsiveness:

  • 0.9-1.0: Very responsive but noisy — use for collision detection
  • 0.5-0.8: Balanced — use for force control
  • 0.1-0.3: Very smooth but laggy — use for slow-changing measurements

Common mistake: Not filtering force/torque sensor data. Raw readings are noisy, and noise causes false safety triggers and jittery force control. Always filter.


ForceCommand

Commands for force-controlled actuators.

.force_only(target) — Pure Force Command

cmd = ForceCommand.force_only(Vector3(x=0.0, y=0.0, z=-10.0))
# Push down with 10N

Commands the actuator to apply the specified force. The controller maintains the target force using feedback from a force/torque sensor.

.surface_contact(normal_force, normal) — Follow a Surface

cmd = ForceCommand.surface_contact(
    normal_force=5.0,
    normal=Vector3(x=0.0, y=0.0, z=1.0),  # Surface normal points up
)

Creates a compliant contact command — the robot pushes against a surface with constant force along the surface normal while allowing free motion in the tangential plane. Essential for surface polishing, assembly insertion, and inspection tasks.

.with_timeout(seconds) — Safety Timeout

cmd = ForceCommand.force_only(Vector3(x=0.0, y=0.0, z=-10.0)) \
    .with_timeout(5.0)

Always set a timeout on force commands. Without one, the robot pushes indefinitely — if the target surface moves away, the robot lunges forward into free space at full force.


ImpedanceParameters

Spring-damper model for compliant robot behavior. Think of it as a virtual spring between the robot and its target position.

.compliant() — Soft Spring (Safe for Contact)

params = ImpedanceParameters.compliant()

Low stiffness, high damping. The robot yields easily on contact — like pushing against a foam pad. Use this when the robot is near humans or during the approach phase of contact tasks.

.stiff() — Rigid Spring (Precise Positioning)

params = ImpedanceParameters.stiff()

High stiffness, low damping. The robot holds position rigidly — small forces won't displace it. Use this for precise positioning in free space (not during contact — rigid + contact = high forces).

.enable() / .disable() — Toggle Impedance Mode

params = ImpedanceParameters.compliant()
params.enable()   # Activate compliance
# ... do contact task ...
params.disable()  # Back to position control

Common mistake: Using stiff() during contact approach. If the robot touches something while stiff, the contact force spikes immediately. Always approach in compliant() mode, then switch to stiff() after stable contact is established.


HapticFeedback

Haptic patterns for teleoperation — the operator feels what the robot feels.

.vibration(intensity, frequency, duration) — Continuous Vibration

vib = HapticFeedback.vibration(intensity=0.8, frequency=200.0, duration=0.5)
  • intensity: 0.0 (off) to 1.0 (max). Clamped.
  • frequency: Hz. 100-300Hz is most perceptible on most haptic devices.
  • duration: seconds.

Use for collision warnings, proximity alerts, motor stall notification.

.force(force_vec, duration) — Force Feedback

ff = HapticFeedback.force(force=Vector3(x=0.0, y=0.0, z=-2.0), duration=1.0)

Pushes back on the operator's hand. Use to convey the robot's contact force — the operator feels resistance proportional to what the robot feels.

.pulse(intensity, frequency, duration) — Single Pulse

pulse = HapticFeedback.pulse(intensity=1.0, frequency=50.0, duration=0.1)

A brief, sharp pulse. Use for event notification — button click confirmation, waypoint reached, object grasped.


ContactInfo

Contact detection and classification from force/torque sensors or contact sensors.

.is_in_contact() — Is the Robot Touching Something?

if contact.is_in_contact():
    print("Contact detected!")

Returns True when the contact state indicates active contact (not NoContact).

.contact_duration_seconds() — How Long Has Contact Lasted?

duration = contact.contact_duration_seconds()
if duration > 2.0:
    print("Stable contact for 2+ seconds — safe to increase force")

Returns seconds since initial contact. Use to distinguish brief collisions (< 0.1s) from stable grasps (> 1s).


TactileArray

Grid of force readings from a tactile sensor pad — common on robotic gripper fingertips.

Constructor

tactile = TactileArray(rows=4, cols=4)  # 4×4 taxel grid

.set_force(row, col, force) / .get_force(row, col) — Access Taxels

tactile.set_force(1, 2, 3.5)           # Set taxel at row 1, col 2 to 3.5 N
force = tactile.get_force(1, 2)         # 3.5
force = tactile.get_force(10, 10)       # None (out of bounds)

Row-major grid. Each taxel is a force reading in Newtons. physical_size gives the sensor pad dimensions in meters, center_of_pressure gives the weighted average contact point.


See Also