Repository navigation
Package delivery, drone magnet fails to re-grab the box after a reset performed mid-carry #3932
Description
Branches
RoboticsAcademy: branchpackage-deliveryRoboticsInfrastructure(submodule): branchdrone-amazon-delivery
Summary
In the package_delivery exercise, the drone uses an electromagnet (drone_gripper
plugin, RoboticsInfrastructure/Industrial/drone_gripper/src/drone_gripper.cpp) to pick
up a box (package_box_01) via a gz::sim::components::DetachableJoint created
dynamically between the drone's magnet link and the box's canonical link.
Symptom: the first time the drone grabs the box in a session, everything works
correctly (both the C++ and Python exercise variants). If at any point a reset is
triggered while the drone is carrying the box, from then on the magnet silently stops
being able to re-grab it: no errors are raised, and in some cases the state topic even
reports attached: true for several seconds straight without the box moving physically.
Architecture before vs. now
-
Before commit
6f32e1be4("make robot, world independent"), the drone was a static
part of the world, just like the box. A reset never destroyed or recreated either
entity iit only restored poses/velocities via gz-sim's native reset
(WorldReset(all=true), equivalent to aSetStaterestore against the initial
snapshot). TheDetachableJointwas always created and destroyed between a parent
(drone) and a child (box) that both stayed alive across resets. -
Now, the reset (orchestrated from RoboticsApplicationManager,
manager/manager.py::reset_sim()+manager/launcher/launcher_gzsim.py::LauncherGzsim.reset())
does, in this order:WorldControl(pause=True)— pause the world./world/default/removeon the robot entity — fully deletes the drone model.WorldControl(pause=True, reset=WorldReset(all=True))— native reset, restoring any
entity present in the world's initial snapshot (including the box, which is never
deleted, only pose-reset).- The backend respawns the drone with a fresh
ros2 launch(a brand-new spawn, new
entity).
DetachableJoint connects two bodies by internally reparenting (at the physics-engine
level, dartsim) the box's body under the drone's skeleton while attached. On detach, the
plugin only removes the ECM DetachableJoint component
(_ecm.RequestRemoveEntity(activeJoint)), asking Physics to undo that reparenting.
The problem is the timing window: that detach happens in the same cycle (world paused) in
which, almost immediately after, the parent side (drone) gets fully destroyed so it
can be recreated from scratch. If the Physics system doesn't get to fully process/apply
the box's "un-reparenting" before the parent skeleton disappears, the box is left with an
inconsistent internal physics state: the ECM correctly reflects that there's no joint
anymore, but the box's body in the physics engine ends up in a corrupted state relative to
a parent-child relationship that no longer exists. A later DetachableJoint on the same
link is created fine at the data level (which is why the state topic can report
attached: true), but the physics engine never actually enforces the constraint.
DetachableJoint is not designed or tested for the case "one of the two ends literally
stops existing mid-detach" only for the case "both ends persist and get their pose
reset", which is exactly the old architecture's scenario.
History of previous fix attempts (all in RoboticsInfrastructure, drone-amazon-delivery branch)
Previously merged commits that progressively hardened the plugin for this transition:
fc107209a— addsISystemReset, detach + state cleanup on reset.27c376629— detects the drone being removed in the SAME cycle (EachRemoved) instead
of a cycle late, so the joint doesn't outlive its links.691692a78— prevents the plugin's ROS thread from becoming a zombie (duplicate
drone_gripper_<model>node) that blocks the respawn.777732771— detach while the world is paused, before the drone gets removed.fbad6a019— forcesmagnetEnabled = falsewhile paused (otherwiseTryAttachwould
auto-reattach to a stale object right on unpause).596d81f5c— explicit ROS node teardown (dead_flag) to avoid zombies once the
drone's entity is removed.
Additional fix from this session (applied and currently active):
- Publish
attached: falseimmediately on a reset-forced detach, instead of waiting
for the periodic heartbeat (which never runs, since the instance dies first) — avoids
the ROS2-side HAL being left with a staleis_carrying() == Trueafter reset.
Attempt that did NOT work (reverted)
In addition to removing the joint, an attempt was made to also delete the box entity
at the same point (inside HandleResetDetach, paused/GripperGoneOrRemoving path),
relying on the WorldReset(all=true) the backend issues right after to recreate the box
from the initial snapshot with a clean physics body — matching the box's treatment to the
drone's own (which is also deleted and recreated).
Conceptually this was the right direction (if the parent side is destroyed and recreated,
the child side should be too, avoiding the asymmetry described above). In practice,
though, after several repeated "grab box → reset mid-carry" cycles, gzserver itself
(the gz sim -s process) ended up dying completely (stops showing up in ps aux, and
the scene launcher (ros2 launch package_delivery.launch.py) never relaunches it).
Suspicion: deleting a model entity within that same paused window competes/interferes with
gz-sim's own internal restore mechanism, more severely than deleting just the joint. No
crash log was captured (gzserver's stdout doesn't land in any accessible file in the dev
container — see the "How to debug" section below) — this is a strong correlation-based
hypothesis, not a confirmed cause backed by a stack trace.
This part has been reverted (only the joint removal + immediate attached:false
publish remain). With that, reset no longer brings down gzserver, but the original bug
(the box can't be re-grabbed after a reset performed mid-carry) is still unresolved.
Reproduction
- Launch the
package_deliveryexercise. - Have the drone grab the box (
enable_magnet()near the box untilis_carrying()
becomestrue). - With the box attached, trigger a reset from the RA UI (or directly via
reset_sim()/LauncherGzsim.reset()). - Try to grab the box again (
enable_magnet()near it). - Observed result: either it never reattaches (physical symptom: the box stays on the
ground, the drone flies off alone), or theattachedtopic reportstruewith no real
physical movement of the box (confirmed live withros2 topic echo:attached: true
held for ~5.5s straight while the box never moved in the viewport).
files
RoboticsInfrastructure/Industrial/drone_gripper/src/drone_gripper.cpp— the magnet
plugin (ISystemPreUpdate/ISystemReset,TryAttach/Detach/HandleResetDetach).RoboticsInfrastructure/database/worlds.sql— the "Quadrotor Gripper" robot row
(id 31):entity=drone,extra_configincludesgripper:=true.RoboticsInfrastructure/CustomRobots/quadrotor/models/quadrotor/quadrotor_common.urdf.xacro
— magnet geometry (magnet_ring_seg,magnetlink,magnetic_gripper).RoboticsApplicationManager/manager/manager.py::reset_sim()and
manager/launcher/launcher_gzsim.py::LauncherGzsim.reset()— the actual reset
orchestration (pause → remove robot → WorldReset(all=true) → respawn robot). Not
freely modifiable from this repo (separate project).
Activity
SanketKokate712 commentedon Sep 1, 2026
I am very interested in contributing to the resolution of this issue regarding the drone magnet's failure to regrab the box following a reset performed during transport. I would like to assist in investigating the physics engine state inconsistencies or exploring alternative methods for managing the DetachableJoint during the reset cycle. Could you please assign this issue to me or provide guidance on how I may begin working on a solution?
rubikscube05 commentedon Sep 9, 2026
Hi team, I would love to help resolve this issue. I am currently setting up my local workspace to reproduce the timing window bug between the physics engine and the ECM during the reset cycle. Drawing from my background troubleshooting flight controllers and managing simulation profiles in Gazebo, I plan to investigate bypassing the ECM delay to detach the bodies directly at the physics level before the drone entity is removed. Let me know if I can take this on
aquintan4 commentedon Sep 11, 2026
@SanketKokate712 @rubikscube05 Thank you both for your interest in contributing. Community PRs are always welcome, so feel free to give it a try and good luck!
Before jumping in, please ensure you thoroughly read the documentation and understand the architecture and interaction between Robotics Academy and Robotics Infrastructure. It's a complex ecosystem, and having a solid grasp of it first will save everyone time.
Feel free to open a PR if you make progress!
FedericoCelauro commentedon Oct 7, 2026
Hi @aquintan4, I'd like to look into this reset issue with the drone magnet. Could you confirm if anyone is working on it?"
javizqh commentedon Oct 7, 2026
@FedericoCelauro right now no one is working on it
integration: add regression test for drone gripper re-grab after mid-…
FedericoCelauro commentedon Oct 21, 2026
Hi, I traced this issue to detachable-joint cleanup in Gazebo Harmonic (
gz-sim8) and implemented a local fix. I cannot open a pull request to the gazebo repository because they require all PR to target the main branch (gz-sim10), so I will briefly describe my fix.The reset removes the drone and later respawns it, while restoring the existing box to its initial state. I found two problems: the physics system removes the drone before detaching the box, and it retains the detached joint’s mapping afterward. When reset reuses that joint entity ID, Gazebo skips creating the new physics joint because the ID is already mapped.
The fix changes the following files:
1.
src/EntityComponentManager.cc— schedule connected joints for removalIn
EntityComponentManager::RequestRemoveEntity(), after collecting the entities to remove, I scan their connections throughcomponents::DetachableJoint.If either a joint’s
parentLinkorchildLinkbelongs to the removal set, I also schedule that joint entity for removal, respecting pinned entities.This is necessary because a detachable joint can connect two models without being a descendant of either model. Recursively deleting the drone therefore does not automatically delete the separate joint entity.
2.
src/systems/physics/Physics.cc— detach before deleting models, then clear the mappingsIn
PhysicsPrivate::RemovePhysicsEntities(), I moved processing of removed detachable joints before processing removed models. This lets DART detach the box while the drone’s physics model still exists.Immediately after detachment, I also remove the joint’s cached mappings:
Without this cleanup, reset can reuse the old joint entity ID, and
CreatePhysicsEntities()skips the new joint becauseentityJointMapstill contains that ID.Before:
drone_joint_bug.mp4
After:
drone_joint_fix.mp4
The removal-order problem is related to [gazebosim/gz-sim#2624], addressed by [PR #2686]. Newer Gazebo versions already include automatic removal of connected joints and detachment before model removal. However, I have not tested this complete scenario on those versions, and [current upstream code] still lacks the mapping cleanup above.
This remains a local Harmonic patch. Following [Gazebo’s contribution policy], an upstream PR would need to target the latest supported compatible version first, with a Harmonic backport considered afterward.