ROS 2 for Robot Arms, bonus step
Bonus: A pick-and-place system
Split pick and place into perception, planning and picking nodes that cooperate over topics, and clear the table.
A system of nodes
Real robots split work into nodes that each do one job and agree only on topics:
| Node | Listens to | Says |
|---|---|---|
/perception | /detect_balls (a service) | /targets: the balls on the table (PoseArray) |
/planner | /targets, /pick_done | /pick_target: the next ball (PointStamped) |
/picker | /pick_target | /pick_done (std_msgs/Bool), after driving the actions |
Any node can be replaced (a camera for /perception, a smarter planner) without touching the others. The planner ignores /targets stamped before the last /pick_done: they were seen before that pick finished, and may still list the ball it took.
Waiting without blocking
A pick takes seconds, but a callback mustn't block. Make it a coroutine: async def on_target(self, msg), with await on every future, such as r = await client.call_async(req). While it waits, the executor runs other callbacks.
There's one catch, the same on a real robot. A coroutine callback keeps its callback group busy until it finishes, and a node's callbacks all share one mutually exclusive group by default. The reply it awaits arrives through its client, which is in that same busy group, so it never gets through: a deadlock. Here a warning names it. Put the clients in another group, such as a ReentrantCallbackGroup(), whose callbacks may run at any time.
Configuration
The launch file sets parameters, which each node declares:
/**:
ros__parameters:
cup: [-0.06, 0.34, 0.0]
perception:
ros__parameters:
rate: 2.0
picker:
ros__parameters:
hover: 0.1
lift: 0.2
move_time: 1.0
/** applies to every node. The gripper keeps its allow_stalling from step 7.
Your task
Clear the table: every ball in the cup within 36 s. Publish the targets, choose the ball nearest the cup, give the picker's clients their own group, and write the pick sequence.