You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 a SetState restore against the initial
snapshot). The DetachableJoint was 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/remove on 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 — adds ISystemReset, 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 — forces magnetEnabled = false while paused (otherwise TryAttach would
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 stale is_carrying() == True after 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_delivery exercise.
Have the drone grab the box (enable_magnet() near the box until is_carrying()
becomes true).
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 the attached topic reports true with no real
physical movement of the box (confirmed live with ros2 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_config includes gripper:=true.
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).
Branches
RoboticsAcademy: branchpackage-deliveryRoboticsInfrastructure(submodule): branchdrone-amazon-deliverySummary
In the
package_deliveryexercise, the drone uses an electromagnet (drone_gripperplugin,
RoboticsInfrastructure/Industrial/drone_gripper/src/drone_gripper.cpp) to pickup a box (
package_box_01) via agz::sim::components::DetachableJointcreateddynamically between the drone's
magnetlink 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: truefor several seconds straight without the box moving physically.Architecture before vs. now
Before commit
6f32e1be4("make robot, world independent"), the drone was a staticpart 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 initialsnapshot). The
DetachableJointwas 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 anyentity present in the world's initial snapshot (including the box, which is never
deleted, only pose-reset).
ros2 launch(a brand-new spawn, newentity).
DetachableJointconnects two bodies by internally reparenting (at the physics-enginelevel, dartsim) the box's body under the drone's skeleton while attached. On detach, the
plugin only removes the ECM
DetachableJointcomponent(
_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
DetachableJointon the samelink 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.DetachableJointis not designed or tested for the case "one of the two ends literallystops 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-deliverybranch)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) insteadof a cycle late, so the joint doesn't outlive its links.
691692a78— prevents the plugin's ROS thread from becoming a zombie (duplicatedrone_gripper_<model>node) that blocks the respawn.777732771— detach while the world is paused, before the drone gets removed.fbad6a019— forcesmagnetEnabled = falsewhile paused (otherwiseTryAttachwouldauto-reattach to a stale object right on unpause).
596d81f5c— explicit ROS node teardown (dead_flag) to avoid zombies once thedrone's entity is removed.
Additional fix from this session (applied and currently active):
attached: falseimmediately on a reset-forced detach, instead of waitingfor the periodic heartbeat (which never runs, since the instance dies first) — avoids
the ROS2-side HAL being left with a stale
is_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/GripperGoneOrRemovingpath),relying on the
WorldReset(all=true)the backend issues right after to recreate the boxfrom 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,
gzserveritself(the
gz sim -sprocess) ended up dying completely (stops showing up inps aux, andthe 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 devcontainer — 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:falsepublish 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
package_deliveryexercise.enable_magnet()near the box untilis_carrying()becomes
true).reset_sim()/LauncherGzsim.reset()).enable_magnet()near it).ground, the drone flies off alone), or the
attachedtopic reportstruewith no realphysical movement of the box (confirmed live with
ros2 topic echo:attached: trueheld for ~5.5s straight while the box never moved in the viewport).
files
RoboticsInfrastructure/Industrial/drone_gripper/src/drone_gripper.cpp— the magnetplugin (
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()andmanager/launcher/launcher_gzsim.py::LauncherGzsim.reset()— the actual resetorchestration (pause → remove robot → WorldReset(all=true) → respawn robot). Not
freely modifiable from this repo (separate project).
package_delivery_bug-2026-07-21_10.13.37.mp4