Skip to content

Work with one arm

Use a Python client on your workstation to connect to the daemon running on the NUC. The NUC maintains the robot's low-level control loop.

Choose an interface

Interface Use it for
Robot Controller parameters, state reception, and direct command streaming
Arm Pose-oriented operations such as observe, move_to, and hold
RobotAgent Policy experiments using reset, observe, and step

All three use the same single-arm transport. Start with Robot when diagnosing a connection, and use Arm when your application works primarily with poses.

Read state first

Complete installation and connection, then run:

from fr3_stack import Robot

with Robot("192.168.1.8") as robot:  # NUC address
    state = robot.wait_for_state(timeout=5.0)
    print(state.pos, state.quat_xyzw)
    print(state.running, state.last_error)

Positions are meters in the robot base frame. Quaternions are [x, y, z, w]. wait_for_state waits for an initial valid state; it does not guarantee freshness throughout a disconnected session. Check received_at with a local monotonic clock when freshness matters.

Send a Cartesian target

The following holds the received pose using Cartesian impedance. It sends a control command; run it only with the intended robot ready for your experiment.

from fr3_stack import Robot

with Robot("192.168.1.8") as robot:
    state = robot.wait_for_state()
    if not state.running or state.last_error:
        raise RuntimeError(state.last_error or "Robot control is not running")
    robot.send_cartesian_impedance(
        target_pos=state.pos,
        target_quat_xyzw=state.quat_xyzw,
    )

The daemon keeps the latest target until another command changes it. Closing a Python connection does not stop the daemon or undo its last command.

Hybrid force/position control

Use Robot.send_hybrid_force_position to select force or position control for each axis. The six entries of S follow [x, y, z, rx, ry, rz]: 1 selects position control and 0 selects force control. target_force is a six-component wrench in the robot base frame, with forces in N and torques in N·m.

For a prepared contact task, this command requests +5 N along base-frame Z while holding the other axes at the captured pose. It requires a configured, calibrated F/T sensor and sends a force-control command:

# Inside an established Robot connection, with the contact setup ready:
state = robot.wait_for_state(timeout=5.0)
robot.send_hybrid_force_position(
    target_pos=state.pos,
    target_quat_xyzw=state.quat_xyzw,
    S=[1, 1, 0, 1, 1, 1],
    target_force=[0, 0, 5, 0, 0, 0],
)

The requested wrench is the force the robot applies to the environment; the sensor measures the opposite reaction. Force targets on position-controlled axes are ignored. The daemon retains the command after the client disconnects.

For a custom axis decomposition, use Robot.send_hybrid with Tr and n_af. See the hybrid controller reference for its inner admittance loop, outer impedance loop, gains, and force conventions.

Work with poses

from fr3_stack import Arm

with Arm("192.168.1.8") as arm:
    observation = arm.observe()
    print(observation.pose.pos)
    arm.hold()  # command a hold at the current pose

Use arm.move_to(target_pose, duration=...) for setup moves and arm.send(pose) for streaming targets. Choose move duration and workspace limits for your setup; there is no universal safe duration for every displacement or load.

Tune the right controller

Defaults live in fr3_stack/configs/. Parameters supplied to Robot.send_* are usually sticky: omitted values reuse the previous controller cache. Loading a profile replaces that cache. send_move_to gain overrides are per-call.

Read the controller reference before choosing gains or force modes. Hybrid control requires F/T data unless its sensor requirement is explicitly disabled for debugging.

Move to two arms

Keep one Robot connection per NUC and add the dual-arm coordinator above those clients. The single-arm controller interface stays the same.