Skip to content

Add option to timestamp joint_states from hardware measurement-time interfaces - #2471

Open
JenniferBuehler wants to merge 9 commits into
ros-controls:masterfrom
JenniferBuehler:jb_measurement_timestamp
Open

Add option to timestamp joint_states from hardware measurement-time interfaces#2471
JenniferBuehler wants to merge 9 commits into
ros-controls:masterfrom
JenniferBuehler:jb_measurement_timestamp

Conversation

@JenniferBuehler

@JenniferBuehler JenniferBuehler commented Jul 13, 2026

Copy link
Copy Markdown

Description

The current state of the joint_state_broadcaster uses now() to stamp the joint states. Some hardware however provides encoder measurement timestamps which the user may want to use instead, to get the true measurement time. This PR adds an option to reach such measurement time from the state interfaces optionally: if the hardware interface doesn't declare or write it, it falls back to the current behavior, so this wouldn't break any existing use of the joint state broadcaster.

Is this user-facing behavior change?

It's an optional feature, while it's user facing (the user being the one writing the hardware interfaces), it's fully backwards compatible.

Did you use Generative AI?

Claude Opus 4.8 was used to execute an implementation plan that was done by the human (myself) and then questioned quite a bit and iterated on, and finally edited, verified and tested by the human (also myself).

Additional Information

This is a PR to rolling for convention, though for our use case it is required in jazzy so it would be desirable if - once approved - we could backport this.

Test plan

1. Unit tests

New cases cover the feature (should pass in CI). Please see inline comments.

2. Interactive test with mock hardware [optional]

If the unit tests are considered insufficient, here is a suggestion on how this can be tested interactively with mock_components/GenericSystem to expose the two measurement-time state interfaces with fixed values.

jsb_timestamp_test.urdf

<?xml version="1.0"?>                                                                               
<robot name="TimestampTestRobot">                                                                   
  <link name="base_link"/>                                                                          
  <joint name="joint1" type="revolute">                                                             
    <parent link="base_link"/><child link="link1"/>                                                 
    <axis xyz="0 0 1"/>                                                                             
    <limit effort="1" velocity="1" lower="-3.14" upper="3.14"/>                                     
  </joint>                                                                                          
  <link name="link1"/>                                                                              
                                                                                                    
  <ros2_control name="TimestampTestSystem" type="system">                                           
    <hardware>                                                                                      
      <plugin>mock_components/GenericSystem</plugin>                                                
    </hardware>                                                                                     
    <joint name="joint1">                                                                           
      <command_interface name="position"/>                                                          
      <state_interface name="position"><param name="initial_value">0.5</param></state_interface>    
      <state_interface name="velocity"/>                                                            
    </joint>                                                                                        
    <!-- Two constant state interfaces standing in for a hardware measurement clock -->             
    <sensor name="measurement_clock">                                                               
      <state_interface name="measurement_time_sec" data_type="uint32"><param name="initial_value">1234</param></state_interface>
      <state_interface name="measurement_time_nsec" data_type="uint32"><param name="initial_value">567000000</param></state_interface>
    </sensor>                                                                                       
  </ros2_control>                                                                                   
</robot>        

jsb_timestamp_test.yaml

controller_manager:
  ros__parameters:
    update_rate: 100
    joint_state_broadcaster:
      type: joint_state_broadcaster/JointStateBroadcaster

joint_state_broadcaster:
  ros__parameters:
    timestamp_state_interfaces:
      sec: measurement_clock/measurement_time_sec
      nsec: measurement_clock/measurement_time_nsec

Run it (three terminals with the workspace sourced):

  1. publish the URDF for robot_description
ros2 run robot_state_publisher robot_state_publisher \
    --ros-args -p robot_description:="$(cat src/ros2_controllers/joint_state_broadcaster/jsb_timestamp_test.urdf)"
  1. start the controller manager with the mock hardware
ros2 run controller_manager ros2_control_node \
    --ros-args --params-file src/ros2_controllers/joint_state_broadcaster/jsb_timestamp_test.yaml
  1. load + activate the broadcaster, then inspect the stamp
ros2 run controller_manager spawner joint_state_broadcaster \
    --param-file src/ros2_controllers/joint_state_broadcaster/jsb_timestamp_test.yaml
ros2 topic echo /joint_states --once

Expected: the message header shows the hardware-provided stamp:

  header:
    stamp:
      sec: 1234
      nanosec: 567000000

Negative check (default behaviour preserved):
Remove the entire top-level joint_state_broadcaster: block, leaving only the controller_manager: section from the yaml:

  controller_manager:
    ros__parameters:
      update_rate: 100
      joint_state_broadcaster:
        type: joint_state_broadcaster/JointStateBroadcaster

header.stamp now shows the controller-manager clock (a current time)

Comment thread joint_state_broadcaster/src/joint_state_broadcaster.cpp
@JenniferBuehler

Copy link
Copy Markdown
Author

Update: Through testing and updating I noticed that the original reason to need double interfaces expired. So will now apply simplifications to the PR. Updates coming - and this staying in draft for now.

@JenniferBuehler
JenniferBuehler marked this pull request as ready for review July 17, 2026 17:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant