fr3_lab_stack provides a policy-agnostic robot-side primitive for realizing an
absolute seven-joint FR3 target through the standard MoveIt execution path.
The package does not interpret OpenPI, DROID, SAPS, or other policy actions. Policy-specific action semantics remain outside this repository.
The validated execution chain is:
absolute q_target [7]
|
v
/plan_kinematic_path
|
v
validated RobotTrajectory
|
v
/execute_trajectory
|
v
fr3_arm_controller
|
v
FR3
The implementation intentionally keeps the standard
fr3_arm_controller active. It does not switch controllers, publish directly
to /fr3_arm_controller/joint_trajectory, invoke MoveIt Servo, or use a
separate position controller.
The validated one-shot executable is:
ros2 run fr3_lab_stack fr3_joint_targetIt accepts either an absolute seven-joint target or, for commissioning, a relative seven-joint delta that is converted to an absolute target from a fresh measured state.
Planning is the default behavior. Physical execution requires the explicit
--execute flag.
Example plan-only invocation:
ros2 run fr3_lab_stack fr3_joint_target \
--delta 0.01 0 0 0 0 0 0Example explicit execution:
ros2 run fr3_lab_stack fr3_joint_target \
--delta 0.01 0 0 0 0 0 0 \
--executeThe validated persistent interface is:
ros2 launch fr3_lab_stack joint_target_server.launch.pyThe server defaults to plan-only operation:
execute=False
Physical execution must be enabled explicitly:
ros2 launch fr3_lab_stack joint_target_server.launch.py execute:=trueThe ROS action name is:
/fr3_joint_target
with type:
fr3_lab_stack_interfaces/action/ExecuteJointTarget
The request contains:
string request_id
float64[7] reference_q
float64[7] target_q
builtin_interfaces/Time reference_stamp
The result contains success/outcome, final measured joint state, MoveIt planning/execution status, whether execution was attempted, and strict JSON evidence. Feedback reports the current stage and measured arm state.
Only one target is processed at a time. A concurrent goal is rejected rather than queued. The first server version deliberately rejects external cancellation requests; runtime faults or timeouts can still cancel an underlying MoveIt execution.
fr3_lab_stack_interfaces is currently kept as a local sibling ROS package in
the lab workspace. fr3_lab_stack therefore has a local workspace dependency
on that package until its long-term repository layout is finalized.
The utility and persistent server own robot-specific target realization:
- acquisition of fresh FR3 joint and Franka robot state,
- controller and ROS-graph ownership checks,
- validation of target and joint limits,
- validation that a supplied
reference_qstill matches fresh measured state, - MoveIt planning through
/plan_kinematic_path, - validation of the returned
RobotTrajectory, - a final pre-execution state/health check,
- execution through
/execute_trajectory, - measured execution telemetry,
- final state and Franka-health evidence.
The one-shot utility permits at most one execution goal per invocation. The persistent server permits at most one active target and one underlying execution attempt per accepted goal. Neither path automatically retries or issues a hold trajectory after a failure.
Before planning or execution, the runtime fails closed when required state or control conditions are not satisfied.
The current checks include:
- fresh and finite FR3 joint telemetry,
- stationary-arm requirement,
- active standard
fr3_arm_controller, - no competing direct arm trajectory publisher,
- no active Servo/SpaceMouse command path,
- commandable Franka robot mode,
- no active
current_errors, - no active Franka collision indicators,
- valid joint position and velocity limits from the live robot description,
- enabled MoveIt joint acceleration limits,
- target position within joint bounds,
- validated trajectory start, ordering, timestamps, endpoint, velocity, and acceleration,
- supplied reference timestamp no older than the configured freshness bound,
- measured planning state within
0.002 radof suppliedreference_q, - pre-execution drift from the planning state no greater than
0.002 rad.
Contact indicators and last_motion_errors are retained as evidence but are
not, by themselves, permanent rejection conditions. This distinction is
important for later manipulation experiments in which task contact can be
legitimate.
The current commissioning configuration uses:
MoveIt group: fr3_arm
planning attempts: 1
allowed planning time: 2.0 s
velocity scaling factor: 0.1
acceleration scaling factor: 0.1
joint goal tolerance: 1e-4 rad
stationary threshold: 0.01 rad/s
telemetry/reference freshness: 0.25 s
reference-state tolerance: 0.002 rad
pre-execution drift tolerance: 0.002 rad
execution timeout: 5.0 s
The trajectory validator uses position and velocity bounds from the live FR3
URDF and acceleration limits from the live MoveIt
robot_description_planning.joint_limits parameters.
The repository implementation was validated on the lab FR3 using a small commissioning target:
q_target = q_measured + [0.01, 0, 0, 0, 0, 0, 0] rad
The repository implementation reproduced the previously commissioned planning behavior:
MoveIt result: SUCCESS
trajectory points: 5
trajectory duration: 0.327501 s
J1 planned peak velocity: 0.047625 rad/s
J1 planned peak accel.: 0.375000 rad/s^2
maximum endpoint error: 8.54e-05 rad
execution attempts: 0
A single explicit execution of the same commissioning target succeeded:
ExecuteTrajectory status: SUCCEEDED
MoveIt result: SUCCESS
execution attempts: 1
trajectory points: 5
trajectory duration: 0.325053 s
J1 planned peak velocity: 0.046875 rad/s
J1 planned peak accel.: 0.375000 rad/s^2
J1 measured peak velocity: 0.057912 rad/s
maximum final target error: 0.001050 rad
The pre-execution configuration drift was approximately 5e-6 rad, and no
Franka current error, reflex, collision indicator, or contact indicator was
present before or after execution. control_command_success_rate remained
1.0.
A real ROS action request was sent using a fresh measured reference_q and
timestamp, with the server running in its default execute=False mode.
action result: SUCCEEDED
outcome: plan_only_validated
reference age at execution: 0.001931 s
max |planning_q - reference_q|: 3.68e-06 rad
execution attempts: 0
trajectory points: 5
trajectory duration: 0.327063 s
J1 planned peak velocity: 0.047625 rad/s
J1 planned peak accel.: 0.375000 rad/s^2
maximum endpoint error: 8.56e-05 rad
The requested absolute target preserved the exact commissioning displacement:
target_q[0] - reference_q[0] = 0.01 rad
The same request path was then validated with execute=True. Exactly one
MoveIt execution attempt was made.
action result: SUCCEEDED
outcome: execution_succeeded
MoveIt planning result: SUCCESS
MoveIt execution result: SUCCESS
execution attempts: 1
reference age at execution: 0.001703 s
max |planning_q - reference_q|: 4.92e-06 rad
max pre-execution start drift: 5.04e-06 rad
trajectory points: 5
trajectory duration: 0.327321 s
J1 planned peak velocity: 0.047625 rad/s
J1 planned peak accel.: 0.375000 rad/s^2
J1 measured peak velocity: 0.060924 rad/s
maximum final target error: 0.001272 rad
maximum final |dq|: 0.001807 rad/s
telemetry errors: 0
No Franka current error, reflex, collision indicator, or contact indicator was
present before or after execution. control_command_success_rate remained
1.0. No retry or second hold/motion command was issued.
Measured derivative peaks are sampled telemetry values and are therefore not assumed to be exact continuous-time maxima. Planned and measured dynamics are reported separately.
The commissioning action request used during validation is retained as:
scripts/manual_joint_target_server_regression.py
This is a manual regression/commissioning helper, not the production SAPS
client and not an automatically executed test. It always constructs the known
commissioning target from a fresh /joint_states sample:
target_q = reference_q + [0.01, 0, 0, 0, 0, 0, 0] rad
The helper queries the server's execute parameter before sending a goal. If
physical execution is enabled, it refuses to continue unless the operator also
passes --allow-execution. This prevents a copied plan-only command from
silently becoming a physical-motion command.
The eventual SAPS client must be implemented separately because it owns the DROID/OpenPI action interpretation and experiment-specific admissibility checks.
The lab stack must remain independent of DROID/OpenPI action semantics.
For the SAPS physical integration, SAPS should:
-
obtain a fresh measured arm state
q_ref, -
select the next policy action,
-
compute the DROID target
q_desired = q_ref + 0.2 * clip(u, -1, 1) -
apply policy-side and experiment-specific admissibility checks,
-
send
q_refandq_desiredto the lab-stack executor, -
receive the execution result and measured final state,
-
obtain a new fresh measured state before constructing the next DROID target.
fr3_lab_stack must not receive the full OpenPI action chunk and must not
reimplement the DROID target conversion. If the robot state has drifted beyond
the accepted tolerance relative to the supplied q_ref, the executor rejects
the request rather than recomputing or altering q_desired.
This keeps the scientific policy semantics in saps-openpi-replication and the
robot-specific realization in fr3_lab_stack.
The MoveIt/fr3_arm_controller path remains the validated conservative baseline for commissioning, pose reset, and isolated targets, but it does not satisfy the DROID temporal requirement.
A small commissioning target typically required about 0.33 s of planned trajectory time. More decisively, a real π0.5/DROID-derived physical action required:
planned trajectory duration: 1.1048 s
request-to-result time: 1.6032 s
DROID action period: 0.0667 s
eight-action DROID window: 0.5333 s
Sequential MoveIt planning/execution would therefore change the policy's temporal realization substantially. The trajectory path is retained as a baseline and safe reset mechanism rather than used for 15 Hz policy execution.
For policy-rate execution, fr3_lab_stack now provides:
fr3_lab_stack/StreamingJointImpedanceController
The controller runs in the existing 1 kHz ros2_control loop, claims the seven FR3 effort interfaces, and accepts the latest absolute seven-joint equilibrium target. On activation it initializes q_desired from measured joint state and produces no internal motion. Each valid external target atomically replaces q_desired; targets are not queued, interpolated, rescaled, or converted into trajectories. If updates stop, the last equilibrium target is held.
Hybrid mode uses the Polymetis-style structure
Kp(q) = J(q)^T Kx J(q) + Kq
Kd(q) = J(q)^T Kxd J(q) + Kqd
tau = Kp(q)(q_desired - q) - Kd(q)dq + coriolis
with the configured gains:
Kq = [40, 30, 50, 25, 35, 25, 10]
Kqd = [4, 6, 5, 5, 3, 2, 1]
Kx = [750, 750, 750, 15, 15, 15]
Kxd = [37, 37, 37, 2, 2, 2]
Policy semantics remain outside this package. For DROID/π0.5 integration, the policy layer still constructs each new target from fresh measured state:
q_desired = q_measured + 0.2 * clip(u, -1, 1)
The controller only realizes the supplied absolute target.
A preliminary fixed-gain joint-impedance version was rejected after a zero-target activation produced 13.30 mrad maximum pose drift. The hybrid controller reduced the same commissioning metric to 2.90 mrad, with no external target applied.
Static J1 characterization showed a small equilibrium offset rather than a proportional tracking loss. After returning to approximately the same starting configuration:
command steady displacement steady residual
+5 mrad 2.936 mrad 2.064 mrad
+30 mrad 27.928 mrad 2.072 mrad
These values characterize the tested J1 configuration only; they are not assumed to be universal across joints or configurations.
The first 15 Hz replacement test sent two targets using fresh measured-state anchoring at each tick:
requested period: 66.667 ms
publisher interval: 66.807 ms
publisher interval error: +0.141 ms
accepted target sequences: 1, 2
target-2 displacement from q_measured: 5.000 mrad
desired-target mismatch: 0
The controller therefore supports the required functional target-replacement pattern. Current state telemetry is published at 100 Hz: the first observed target ages (6.9--10.7 ms in the two-target test) are telemetry-quantized observations, not precise target receipt-to-application latency measurements. Exact callback receipt, first-control-cycle application, and 1 kHz controller-period/jitter instrumentation remain follow-on work before quantitative timing claims or the prerecorded eight-action DROID test.
Validated now:
absolute target -> MoveIt/JTC single-target execution
persistent one-goal-at-a-time MoveIt action server
hybrid streaming impedance controller build/tests/plugin loading
zero-target hybrid activation
single-target hybrid execution
two fresh-state-anchored target replacements at 15 Hz
static J1 equilibrium characterization over 5--30 mrad commands
Next:
precise target receipt/application and controller-period timing instrumentation
prerecorded eight-action DROID sequence at 15 Hz
live π0.5 multi-action execution
SAPS arbitration / recovery / resumption integration