Skip to content

Propagate feedback and command errors through the ROS hardware interface - #9

Merged
justagist merged 6 commits into
freshness-snapshotsfrom
ros-error-propagation
Oct 15, 2026
Merged

justagist merged 6 commits into
freshness-snapshotsfrom
ros-error-propagation

Conversation

@justagist

@justagist justagist commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #8 (needs the fixed is_healthy).

  • controlJoints() and controlEndPose() hold a mutex for their three frames, so concurrent
    callers cannot interleave into a target the arm never received from either.
  • write() validates every command before sending anything (finite, within a full turn or a
    metre of jaw) and returns ERROR when a CAN send fails. on_activate() checks its sends too.
  • While active, read() returns ERROR when joint or gripper feedback goes stale instead of
    reporting the cached pose as current. The controller manager then stops the controllers;
    the arm holds its last target, as on deactivation.

Conversions live in piper_cpp_ros/command_conversion.hpp with an ament_cmake_gtest target.
Three new tests.

Summary by Sourcery

Harden the ROS hardware interface by validating and propagating command and feedback failures while preserving coherent arm targets.

Bug Fixes:

  • Propagate CAN send failures and stale joint or gripper feedback as hardware-interface errors so controllers stop instead of using invalid or cached state.
  • Prevent partially applied targets by validating all joint and gripper commands before transmission.

Enhancements:

  • Serialize multi-frame joint and end-pose commands to prevent concurrent callers from interleaving frames.
  • Add hardware error-state cleanup that closes the CAN session while leaving the arm holding its last target.
  • Centralize and test command-unit conversions and wire-range validation.

Build:

  • Add the ament CMake GoogleTest target and test dependency for command conversion tests.

Documentation:

  • Document hardware-interface behavior for stale feedback, invalid commands, and CAN transmission failures.

Tests:

  • Add coverage for atomic multi-frame command transmission under concurrent callers and command conversion validation.

@sourcery-ai

sourcery-ai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The PR hardens the ROS hardware interface by serializing multipart CAN commands, validating complete command sets before transmission, propagating send and feedback-health failures as lifecycle/read/write errors, and adding focused conversion and concurrency tests.

Sequence diagram for validated ROS command transmission

sequenceDiagram
    participant ControllerManager
    participant PiperHardware
    participant PiperInterface
    participant CAN

    ControllerManager->>PiperHardware: write()
    PiperHardware->>PiperHardware: jointCommandToMillideg()
    PiperHardware->>PiperHardware: gripperCommandToMicrometres()
    alt invalid command
        PiperHardware-->>ControllerManager: ERROR
    else valid command set
        PiperHardware->>PiperInterface: controlJoints()
        PiperInterface->>CAN: send multipart joint frames
        PiperInterface-->>PiperHardware: send result
        alt joint send failed
            PiperHardware-->>ControllerManager: ERROR
        else joint send succeeded
            PiperHardware->>PiperInterface: controlGripper()
            PiperInterface->>CAN: send gripper frame
            PiperInterface-->>PiperHardware: send result
            PiperHardware-->>ControllerManager: OK or ERROR
        end
    end
Loading

Sequence diagram for stale feedback stopping active controllers

sequenceDiagram
    participant ControllerManager
    participant PiperHardware
    participant PiperInterface

    ControllerManager->>PiperHardware: read()
    PiperHardware->>PiperInterface: getArmJointStates()
    alt active and joint feedback unhealthy
        PiperHardware-->>ControllerManager: ERROR
    else joint feedback healthy
        PiperHardware->>PiperInterface: getGripperStates()
        alt active and gripper feedback unhealthy
            PiperHardware-->>ControllerManager: ERROR
        else feedback healthy
            PiperHardware-->>ControllerManager: OK with current state
        end
    end
Loading

File-Level Changes

Change Details Files
Serializes multipart motion commands so concurrent callers transmit complete, non-interleaved frame sequences.
  • Adds a mutex covering all three joint-command frames.
  • Adds the same protection for multipart Cartesian end-pose commands.
  • Adds a stress test verifying frame ordering and target consistency.
piper_cpp/include/piper_cpp/interface/piper_interface_v2.h
piper_cpp/src/interface/piper_interface_v2.cpp
piper_cpp/test/piper_interface_v2_test.cpp
Propagates command-send failures and rejects invalid hardware commands before any CAN transmission.
  • Checks motion-mode and activation zeroing sends.
  • Extracts finite/range-checked joint and gripper unit conversions into reusable helpers.
  • Validates the complete joint and gripper command set before sending, then returns ERROR for failed joint or gripper sends.
  • Adds conversion unit tests and registers the ament gtest target.
piper_cpp_ros/include/piper_cpp_ros/command_conversion.hpp
piper_cpp_ros/src/piper_hardware_interface.cpp
piper_cpp_ros/test/command_conversion_test.cpp
piper_cpp_ros/CMakeLists.txt
piper_cpp_ros/package.xml
Treats stale feedback as a hardware failure while the interface is active and documents the resulting controller-stop behavior.
  • Requires healthy, fresh feedback during activation.
  • Returns read() ERROR when active joint or gripper feedback exceeds the freshness threshold.
  • Documents that failed reads/writes stop controllers while the arm retains its last target.
piper_cpp_ros/src/piper_hardware_interface.cpp
piper_cpp_ros/README.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e076ec14-81fa-4e99-8684-4095d7a2b2bd

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Activation checks, lifecycle fault recovery, and concurrency-test failure cleanup remain incomplete.

3 open findings
What changed in this PR

Makes the ROS hardware interface report stale feedback and command failures, building on #8’s freshness tracking.

Changes:

  • Serializes multi-frame joint and Cartesian commands.
  • Validates commands and propagates feedback and transmission errors.
  • Adds conversion helpers, tests, and documentation.
File Description
piper_cpp/​test/​piper_interface_v2_test.cpp Tests concurrent command serialization.
piper_cpp/​src/​interface/​piper_interface_v2.cpp Locks multi-frame transmissions.
piper_cpp/​include/​piper_cpp/​interface/​piper_interface_v2.h Declares the transmission mutex.
piper_cpp_ros/​test/​command_conversion_test.cpp Tests command validation and conversions.
piper_cpp_ros/​src/​piper_hardware_interface.cpp Adds validation, freshness checks, and error propagation.
piper_cpp_ros/​README.md Documents hardware error behavior.
piper_cpp_ros/​package.xml Adds the GoogleTest dependency.
piper_cpp_ros/​include/​piper_cpp_ros/​command_conversion.hpp Centralizes validated unit conversions.
piper_cpp_ros/​CMakeLists.txt Registers conversion tests.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread piper_cpp/test/piper_interface_v2_test.cpp Outdated
Comment thread piper_cpp_ros/src/piper_hardware_interface.cpp
Comment thread piper_cpp_ros/src/piper_hardware_interface.cpp

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 5 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="piper_cpp/src/interface/piper_interface_v2.cpp" line_range="671" />
<code_context>
         joint_1_millideg, joint_2_millideg, joint_3_millideg,
         joint_4_millideg, joint_5_millideg, joint_6_millideg,
     };
+    std::lock_guard<std::mutex> lock(multipart_tx_mutex_);
     msg.type = ArmMsgType::JointCtrl12;
     if (!sendPiperMsg(msg))
</code_context>
<issue_to_address>
**Failed writes leave partial targets**

When a CAN send fails after one or more target frames have been delivered, `controlJoints()` or the gripper send can fail after earlier target frames have reached the arm, and `write()` then returns `ERROR` without undoing them. The arm is left with a partial or mixed target instead of the previous target.

Make failed writes recover from already-delivered target frames so the arm is not left with a partial or mixed target.

Also at `piper_cpp_ros/src/piper_hardware_interface.cpp:485-496`, `piper_cpp_ros/README.md:248`.
</issue_to_address>

### Comment 2
<location path="piper_cpp_ros/src/piper_hardware_interface.cpp" line_range="228" />
<code_context>
     }

-    piper_->sendMotionCtrl2(
-        piper_cpp::CtrlMode::CanCmd, piper_cpp::MoveMode::MoveJ, static_cast<uint8_t>(speed_pct_),
-        piper_cpp::MitMode::PositionVelocity
-    );
</code_context>
<issue_to_address>
**Enable send failure goes unchecked**

When the enable CAN send fails while the robot is already reported enabled, `on_activate()` ignores the result of `enableRobot()` and can proceed when the robot is already reported enabled, even though the enable command failed to send. The later checked motion-mode send does not detect that failure, so activation can succeed without confirming this command.

Check `enableRobot()` and return `CallbackReturn::ERROR` if it fails.
</issue_to_address>

### Comment 3
<location path="piper_cpp_ros/src/piper_hardware_interface.cpp" line_range="247" />
<code_context>
         while (steady_clock::now() - zero_t0 < zero_duration)
         {
-            piper_->controlJoints(0, 0, 0, 0, 0, 0);
+            if (!piper_->controlJoints(0, 0, 0, 0, 0, 0))
+            {
+                RCLCPP_ERROR(rclcpp::get_logger(kLogger), "Failed to send joint targets while driving to zero.");
</code_context>
<issue_to_address>
**Gripper homing failure is ignored**

When `with_gripper` and `home_gripper_on_activate` are enabled and the CAN send for the zero command fails, `on_activate()` discards the false return from `setGripperZero()`, then continues and can report successful activation with the gripper still using its previous zero reference.

Check the return value of `setGripperZero()` and return `CallbackReturn::ERROR` when it fails.
</issue_to_address>

### Comment 4
<location path="piper_cpp/test/piper_interface_v2_test.cpp" line_range="229" />
<code_context>
+    while (frames.size() < frames.capacity())
+    {
+        pollfd pfd{bus.peer, POLLIN, 0};
+        ASSERT_GT(poll(&pfd, 1, 2000), 0) << "bus went quiet after " << frames.size() << " frames";
+        can_frame f{};
+        ASSERT_EQ(::recv(bus.peer, &f, sizeof(f), 0), static_cast<ssize_t>(sizeof(f)));
</code_context>
<issue_to_address>
**Receive failure aborts the test process**

When the receive poll times out or a socket receive fails, a fatal assertion returns from the test before threads `a` and `b` are joined; destroying either still-joinable `std::thread` calls `std::terminate`, aborting the test process instead of reporting a normal test failure.

Ensure both threads are joined even when a receive assertion fails.
</issue_to_address>

### Comment 5
<location path="piper_cpp/test/piper_interface_v2_test.cpp" line_range="225-226" />
<code_context>
+    std::thread b(sender, 2000);
+
+    std::vector<can_frame> frames;
+    frames.reserve(2 * kCallsPerThread * 3);
+    while (frames.size() < frames.capacity())
+    {
+        pollfd pfd{bus.peer, POLLIN, 0};
</code_context>
<issue_to_address>
**Reserved capacity causes false timeouts**

When `frames.capacity()` is greater than the number of frames the senders produce, the loop waits for `frames.capacity()` frames rather than the expected frame count, so `poll()` times out after all sent frames arrive and the test fails on a valid vector implementation.

Use the expected number of frames as the loop limit instead of `frames.capacity()`.
</issue_to_address>

Sourcery assessment

Approval pending. 5 findings to address first.

Blocking findings: piper_cpp/src/interface/piper_interface_v2.cpp:671, piper_cpp_ros/src/piper_hardware_interface.cpp:228, piper_cpp_ros/src/piper_hardware_interface.cpp:247, piper_cpp/test/piper_interface_v2_test.cpp:229, piper_cpp/test/piper_interface_v2_test.cpp:226


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread piper_cpp/src/interface/piper_interface_v2.cpp
Comment thread piper_cpp_ros/src/piper_hardware_interface.cpp
Comment thread piper_cpp_ros/src/piper_hardware_interface.cpp
Comment thread piper_cpp/test/piper_interface_v2_test.cpp Outdated
Comment thread piper_cpp/test/piper_interface_v2_test.cpp Outdated
@justagist
justagist merged commit 49a7ec4 into freshness-snapshots Oct 15, 2026
5 checks passed
justagist added a commit that referenced this pull request Oct 15, 2026
…ace (#9 #10)

* serialise multipart commands so concurrent callers cannot interleave frames

* validate commands and propagate send failures from the hardware interface

* report stale feedback as an error while active

* join the senders before asserting in the interleave test

* check the enable and gripper zero sends during activation

* release the CAN session on error so the component can be reconfigured
@justagist
justagist deleted the ros-error-propagation branch October 19, 2026 08:20
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.

2 participants