activate() and recoverFromFault() block for the length of a calibration sweep, one to two seconds. Any consumer with a control loop has to run them off that loop, and the SDK gives it no help.
The ROS wrapper shows what that costs. It wraps recoverFromFault() in std::async, adds a hasFinished() helper to distinguish "outstanding" from "finished" on the future, and relies on declaration order — recovery_ declared after gripper_ — so that reverse destruction joins the future before the gripper the lambda captured is destroyed. That last one is a dangling-reference hazard avoided by a comment, and every consumer that does the same thing has to rediscover it.
This is not specific to ROS. Any polling consumer (a PLC bridge, a GUI, a robot controller) needs the same thing, and the SDK already runs a thread of its own, so a second thread per call from std::async is the wrong shape.
Proposal
A poll-based handle rather than async twins of each function: start the procedure, then poll it each cycle for InProgress / Activated / Timeout / FaultLatched. Reimplement the existing blocking activate() and recoverFromFault() on top of it so they remain the easy default and the two forms cannot diverge.
Deliberately not std::future: it commits every consumer to <future>, and it is not available freestanding, which matters if the SDK is ever built for an MCU target. A poll-based handle works everywhere and composes with any event loop.
Design questions to settle
- Ownership and lifetime. Destroying a
Gripper with an operation outstanding must be safe and defined, rather than left to the consumer's member ordering.
- Whether the exchange thread drives the procedure or the consumer's
poll() call does the work. The latter keeps the SDK single-threaded from the consumer's point of view.
- Cancellation: whether an outstanding procedure can be abandoned, and what state the gripper is left in if it is.
- At most one outstanding procedure per gripper, and what a second
start() does.
Follow-up
In the ROS wrapper once this lands: drop std::async, hasFinished() and the declaration-order dependency from robotiq_driver's hardware interface.
activate()andrecoverFromFault()block for the length of a calibration sweep, one to two seconds. Any consumer with a control loop has to run them off that loop, and the SDK gives it no help.The ROS wrapper shows what that costs. It wraps
recoverFromFault()instd::async, adds ahasFinished()helper to distinguish "outstanding" from "finished" on the future, and relies on declaration order —recovery_declared aftergripper_— so that reverse destruction joins the future before the gripper the lambda captured is destroyed. That last one is a dangling-reference hazard avoided by a comment, and every consumer that does the same thing has to rediscover it.This is not specific to ROS. Any polling consumer (a PLC bridge, a GUI, a robot controller) needs the same thing, and the SDK already runs a thread of its own, so a second thread per call from
std::asyncis the wrong shape.Proposal
A poll-based handle rather than async twins of each function: start the procedure, then poll it each cycle for
InProgress/Activated/Timeout/FaultLatched. Reimplement the existing blockingactivate()andrecoverFromFault()on top of it so they remain the easy default and the two forms cannot diverge.Deliberately not
std::future: it commits every consumer to<future>, and it is not available freestanding, which matters if the SDK is ever built for an MCU target. A poll-based handle works everywhere and composes with any event loop.Design questions to settle
Gripperwith an operation outstanding must be safe and defined, rather than left to the consumer's member ordering.poll()call does the work. The latter keeps the SDK single-threaded from the consumer's point of view.start()does.Follow-up
In the ROS wrapper once this lands: drop
std::async,hasFinished()and the declaration-order dependency fromrobotiq_driver's hardware interface.