Repository navigation
Propagate feedback and command errors through the ROS hardware interface #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e456ded
9e07e71
d8854a3
e2400ea
a8f70a4
3af310d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,8 +10,10 @@ | |
| #include <future> | ||
| #include <initializer_list> | ||
| #include <memory> | ||
| #include <poll.h> | ||
| #include <sys/socket.h> | ||
| #include <thread> | ||
| #include <vector> | ||
|
|
||
| using namespace std::chrono_literals; | ||
| using piper_cpp::PiperInterfaceV2; | ||
|
|
@@ -202,3 +204,53 @@ TEST(PiperInterfaceV2, DisconnectClearsCachedState) | |
| EXPECT_FALSE(js.is_valid) << "feedback from a closed session must not look current"; | ||
| EXPECT_EQ(js.value.joint_6, 0); | ||
| } | ||
|
|
||
| TEST(PiperInterfaceV2, ConcurrentJointCommandsDoNotInterleave) | ||
| { | ||
| // Each controlJoints() call is three frames (0x155, 0x156, 0x157). Two callers with | ||
| // different targets must still produce whole triples on the bus, never a mix. | ||
| constexpr int kCallsPerThread = 500; | ||
| auto bus = makeBus(); | ||
| ASSERT_TRUE(bus.piper->connectPort(/*can_init=*/false, /*piper_init=*/false, /*start_threads=*/false)); | ||
|
|
||
| auto sender = [&](int32_t value) | ||
| { | ||
| for (int i = 0; i < kCallsPerThread; ++i) | ||
| EXPECT_TRUE(bus.piper->controlJoints(value, value, value, value, value, value)); | ||
| }; | ||
| std::thread a(sender, 1000); | ||
| std::thread b(sender, 2000); | ||
|
|
||
| // Receive failures are recorded rather than asserted here so the senders are always | ||
| // joined; a quiet bus means they have finished, so the joins cannot hang. | ||
| constexpr size_t kExpectedFrames = 2 * kCallsPerThread * 3; | ||
| std::vector<can_frame> frames; | ||
| frames.reserve(kExpectedFrames); | ||
| bool received_all = true; | ||
| while (frames.size() < kExpectedFrames) | ||
| { | ||
| pollfd pfd{bus.peer, POLLIN, 0}; | ||
| can_frame f{}; | ||
| if (poll(&pfd, 1, 2000) <= 0 || ::recv(bus.peer, &f, sizeof(f), 0) != static_cast<ssize_t>(sizeof(f))) | ||
| { | ||
| received_all = false; | ||
|
Comment on lines
+234
to
+236
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · Slow sends fail concurrency test When the sender threads are not scheduled or produce no frame for more than two seconds, Use a completion-aware wait or a more generous overall deadline so a temporary scheduling pause does not fail the test. Prompt for AI agentsIn `piper_cpp/test/piper_interface_v2_test.cpp` at lines 234-236:
**Slow sends fail concurrency test**
When the sender threads are not scheduled or produce no frame for more than two seconds, `ConcurrentJointCommandsDoNotInterleave` marks `received_all` false after any two-second gap between frames, then fails at line 243 even if the correct sender threads resume and complete afterward. A heavily loaded runner can therefore fail this test without a command or interleaving defect.
Use a completion-aware wait or a more generous overall deadline so a temporary scheduling pause does not fail the test. |
||
| break; | ||
| } | ||
| frames.push_back(f); | ||
| } | ||
| a.join(); | ||
|
Comment on lines
+234
to
+241
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · Interleave test can hang on receive error When On receive failure, stop the senders or close the sending socket and ensure blocked writes are released before joining the threads. Prompt for AI agentsIn `piper_cpp/test/piper_interface_v2_test.cpp` at lines 234-241:
**Interleave test can hang on receive error**
When `poll` or `recv` fails before all expected frames have been consumed while sender threads still have frames to write, the receiver breaks out of its loop, then joins both sender threads while their blocking socket writes can still be filling the unread socket buffer. The senders block once that buffer fills, so the joins never return and the test hangs instead of reporting the receive failure.
On receive failure, stop the senders or close the sending socket and ensure blocked writes are released before joining the threads. |
||
| b.join(); | ||
| ASSERT_TRUE(received_all) << "bus went quiet after " << frames.size() << " frames"; | ||
|
|
||
| auto firstJoint = [](const can_frame& f) | ||
| { return static_cast<int32_t>((f.data[0] << 24) | (f.data[1] << 16) | (f.data[2] << 8) | f.data[3]); }; | ||
| for (size_t i = 0; i < frames.size(); i += 3) | ||
| { | ||
| ASSERT_EQ(frames[i].can_id, 0x155u) << "triple " << i / 3 << " does not start with 0x155"; | ||
| ASSERT_EQ(frames[i + 1].can_id, 0x156u); | ||
| ASSERT_EQ(frames[i + 2].can_id, 0x157u); | ||
| const auto target = firstJoint(frames[i]); | ||
| ASSERT_EQ(firstJoint(frames[i + 1]), target) << "triple " << i / 3 << " mixes two callers"; | ||
| ASSERT_EQ(firstJoint(frames[i + 2]), target) << "triple " << i / 3 << " mixes two callers"; | ||
|
Comment on lines
+252
to
+254
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · Wrong targets pass concurrency test When Assert that each triple's target is one of the two requested values, as well as checking consistency across its frames. Prompt for AI agentsIn `piper_cpp/test/piper_interface_v2_test.cpp` at lines 252-254:
**Wrong targets pass concurrency test**
When `controlJoints()` sends the same constant value for every request, `ConcurrentJointCommandsDoNotInterleave` derives `target` from the first observed frame and compares only the other frames against it. If `controlJoints()` serializes the same default value for both callers instead of their requested targets, every comparison still passes.
Assert that each triple's target is one of the two requested values, as well as checking consistency across its frames. |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| #pragma once | ||
|
|
||
| #include <cmath> | ||
| #include <cstdint> | ||
| #include <optional> | ||
|
|
||
| namespace piper_cpp_ros | ||
| { | ||
|
|
||
| constexpr double kRadToMd = 1000.0 * 180.0 / M_PI; | ||
| constexpr double kMdToRad = 1.0 / kRadToMd; | ||
|
|
||
| // piper_cpp's high-speed feedback reports motor_speed in 0.001 rad/s and effort in 0.001 N*m. | ||
| constexpr double kMilliToUnit = 1.0e-3; | ||
|
|
||
| // Gripper feedback / command: jaw stroke is in 0.001 mm = micrometers (1 um = 1e-6 m). | ||
| constexpr double kUmToM = 1.0e-6; | ||
| constexpr double kMToUm = 1.0e6; | ||
|
|
||
| inline double mdToRad(int32_t md) { return static_cast<double>(md) * kMdToRad; } | ||
|
|
||
| /// Joint position command in rad to the arm's 0.001 deg units, or nullopt when the value is not | ||
| /// finite or lies beyond a full turn. Real joint limits are the controller's job; this only | ||
| /// refuses values that have no sensible wire representation. | ||
| inline std::optional<int32_t> jointCommandToMillideg(double rad) | ||
| { | ||
| if (!std::isfinite(rad) || std::fabs(rad) > 2.0 * M_PI) | ||
| return std::nullopt; | ||
| return static_cast<int32_t>(std::lround(rad * kRadToMd)); | ||
| } | ||
|
|
||
| /// Gripper stroke command in metres to micrometres, with the same acceptance rule; one metre | ||
| /// is far beyond any jaw. | ||
| inline std::optional<int32_t> gripperCommandToMicrometres(double m) | ||
| { | ||
| if (!std::isfinite(m) || std::fabs(m) > 1.0) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · Out-of-range jaw targets are sent When a finite gripper command falls outside the configured 0–0.035 m joint limits but remains within ±1 m, Validate gripper commands against the configured 0–0.035 m joint limits before converting them. Prompt for AI agentsIn `piper_cpp_ros/include/piper_cpp_ros/command_conversion.hpp` at line 36:
**Out-of-range jaw targets are sent**
When a finite gripper command falls outside the configured 0–0.035 m joint limits but remains within ±1 m, `gripperCommandToMicrometres()` accepts the command, including negative positions, and `write()` sends the out-of-range jaw target instead of returning an error.
Validate gripper commands against the configured 0–0.035 m joint limits before converting them. |
||
| return std::nullopt; | ||
| return static_cast<int32_t>(std::lround(m * kMToUm)); | ||
| } | ||
|
|
||
| } // namespace piper_cpp_ros | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Medium · Failed writes apply partial targets
When a CAN send fails after an earlier joint frame or actuator target in the same write was accepted,
controlJoints()can return false after earlier joint frames were sent, andwrite()sends the joints before the gripper. ReturningERRORand closing the session cannot undo those sends, so the arm receives a partial command instead of holding its last complete target, contrary to the README contract.Make failed joint and gripper writes leave or restore the last complete target rather than relying on returning
ERRORor closing the session.Also at
piper_cpp_ros/src/piper_hardware_interface.cpp:508,piper_cpp_ros/src/piper_hardware_interface.cpp:511-513,piper_cpp_ros/src/piper_hardware_interface.cpp:516,piper_cpp_ros/src/piper_hardware_interface.cpp:518,piper_cpp_ros/README.md:248.Prompt for AI agents