mj-kdl-wrapper  0.1.0
MuJoCo + KDL bridge for robot kinematics and dynamics
Loading...
Searching...
No Matches
ex_pick.cpp
Go to the documentation of this file.
1/* ex_pick.cpp
2 * Scripted pick-and-place: Kinova GEN3 + Robotiq 2F-85 picks an orange cube
3 * from the floor and lifts it.
4 *
5 * Joint impedance control law applied every step:
6 * tau_i = g_kdl_i + Kp*(q_des_i - q_i) - Kd*dq_i
7 * where g_kdl is computed via KDL::ChainDynParam::JntToGravity (includes gripper inertia).
8 *
9 * A state machine drives the arm through six states, each with its own
10 * while control loop:
11 * HOME -> PREGRASP -> GRASP -> CLOSE -> LIFT -> HOLD
12 *
13 * Requires third_party/menagerie (MuJoCo Menagerie submodule).
14 *
15 * Usage:
16 * ex_pick [--headless]
17 *
18 * With --headless runs the full pick sequence and prints final cube height. */
19
21
22#include <kdl/chaindynparam.hpp>
23#include <kdl/chainfksolverpos_recursive.hpp>
24#include <kdl/chainiksolverpos_nr_jl.hpp>
25#include <kdl/chainiksolvervel_pinv.hpp>
26
27#include <algorithm>
28#include <cmath>
29#include <filesystem>
30#include <iomanip>
31#include <iostream>
32#include <string>
33
34// scene constants
35static constexpr double kHomePose[7] = { 0.0, 0.2618, 3.1416, -2.2689, 0.0, 0.9599, 1.5708 };
36static constexpr double kCubeX = 0.4;
37static constexpr double kCubeY = 0.0;
38static constexpr double kCubeHS = 0.02; // half-size of 4 cm cube
39static constexpr double kCubeZ = kCubeHS;
40static constexpr double kGripperReach = 0.18422; // bracelet_link -> pad reach along gripper Z
41
42// impedance gains (per joint, matching ex_impedance tuning)
43static constexpr double kKp[7] = { 100, 200, 100, 200, 100, 200, 100 }; // Nm/rad
44static constexpr double kKd[7] = { 10, 20, 10, 20, 10, 20, 10 }; // Nm*s/rad
45
46// helpers
47namespace fs = std::filesystem;
48static fs::path repo_root() { return fs::path(__FILE__).parent_path().parent_path().parent_path(); }
49static double clamp01(double v) { return std::max(0.0, std::min(1.0, v)); }
50
51static void lerp_q(const KDL::JntArray &a, const KDL::JntArray &b, double t, KDL::JntArray &out)
52{
53 for (unsigned i = 0; i < a.rows(); ++i) out(i) = a(i) + t * (b(i) - a(i));
54}
55
56/*
57 * Compute joint impedance torques into robot.jnt_trq_cmd:
58 * tau_i = g_kdl[i] + Kp[i]*(q_des_i - q_msr_i) - Kd[i]*dq_msr_i
59 * dyn must have been built from the chain returned by init_robot_from_mjcf with tool_body set.
60 */
61static void impedance_ctrl(
62 mj_kdl::Robot &robot,
63 const KDL::JntArray &q_des,
64 unsigned n,
65 KDL::ChainDynParam &dyn
66)
67{
68 KDL::JntArray q(n), g(n);
69 for (unsigned i = 0; i < n; ++i) q(i) = robot.jnt_pos_msr[i];
70 dyn.JntToGravity(q, g);
71 for (unsigned i = 0; i < n; ++i) {
72 robot.jnt_trq_cmd[i] =
73 g(i) + kKp[i] * (q_des(i) - robot.jnt_pos_msr[i]) - kKd[i] * robot.jnt_vel_msr[i];
74 }
75}
76
77// Copy the current measured joint positions into q.
78static void snapshot_q(const mj_kdl::Robot &robot, unsigned n, KDL::JntArray &q)
79{
80 for (unsigned i = 0; i < n; ++i) q(i) = robot.jnt_pos_msr[i];
81}
82
83static double max_abs_joint_err(const mj_kdl::Robot &robot, const KDL::JntArray &q_des, unsigned n)
84{
85 double max_err = 0.0;
86 for (unsigned i = 0; i < n; ++i)
87 max_err = std::max(max_err, std::abs(q_des(i) - robot.jnt_pos_msr[i]));
88 return max_err;
89}
90
91// state machine
93
94/*
95 * Static description of one state: how long to stay, which joint target to
96 * track (pointer into the IK-solved arrays), gripper command, and successor.
97 */
99{
100 double duration; // nominal interpolation time
101 double timeout; // force transition if settling runs long
102 double settle_tol; // rad, < 0 disables pose check
103 const KDL::JntArray *q_target; // interpolation goal (set after IK)
104 double gripper_cmd; // 0=open, 255=fully closed
106};
107
108/*
109 * Mutable context updated on every state transition.
110 * q_enter is the measured joint position when the state was entered;
111 * the setpoint is linearly interpolated from q_enter to q_target over duration.
112 */
114{
116 double t_enter = 0.0;
117 KDL::JntArray q_enter;
118};
119
120int main(int argc, char *argv[])
121{
122 bool headless = false;
123 for (int i = 1; i < argc; ++i)
124 if (std::string(argv[i]) == "--headless") headless = true;
125
126 const fs::path root = repo_root();
127 if (!fs::exists(root / "third_party/menagerie")) {
128 std::cerr << "third_party/menagerie/ not found - run: "
129 "git submodule update --init third_party/menagerie\n";
130 return 1;
131 }
132
133 // scene setup
134 const std::string arm_mjcf = (root / "third_party/menagerie/kinova_gen3/gen3.xml").string();
135 const std::string grp_mjcf = (root / "third_party/menagerie/robotiq_2f85/2f85.xml").string();
136
138 gs.mjcf_path = grp_mjcf.c_str();
139 gs.attach_to = "bracelet_link";
140 gs.prefix = "g_";
141 gs.pos[2] = -0.061525;
142 gs.euler[0] = 180.0; // flip gripper around X
143
145 rs.path = arm_mjcf.c_str();
146 rs.attachments.push_back(gs);
147
149 cube.name = "cube";
151 cube.size[0] = cube.size[1] = cube.size[2] = kCubeHS;
152 cube.pos[0] = kCubeX;
153 cube.pos[1] = kCubeY;
154 cube.pos[2] = kCubeZ;
155 cube.rgba[0] = 1.0f;
156 cube.rgba[1] = 0.5f;
157 cube.rgba[2] = 0.0f;
158 cube.rgba[3] = 1.0f;
159 cube.mass = 0.1;
160 cube.condim = 4;
161 cube.friction[0] = 0.8;
162 cube.friction[1] = 0.02;
163 cube.friction[2] = 0.001;
164
166 sc.robots.push_back(rs);
167 sc.objects.push_back(cube);
168
169 mjModel *model = nullptr;
170 mjData *data = nullptr;
171 if (!mj_kdl::build_scene(&model, &data, &sc)) {
172 std::cerr << "build_scene() failed\n";
173 return 1;
174 }
175
176 mj_kdl::Robot robot;
178 &robot, model, data, "base_link", "bracelet_link", "", "g_base"
179 )) {
180 std::cerr << "init_robot_from_mjcf() failed\n";
181 mj_kdl::destroy_scene(model, data);
182 return 1;
183 }
184
185 unsigned n = robot.chain.getNrOfJoints();
186 int fingers_act = mj_name2id(model, mjOBJ_ACTUATOR, "g_fingers_actuator");
187 int cube_jnt = mj_name2id(model, mjOBJ_JOINT, "cube_joint");
188 int key_id = mj_name2id(model, mjOBJ_KEY, "home");
189
190 KDL::ChainDynParam dyn(robot.chain, KDL::Vector(0.0, 0.0, -9.81));
191
192 // IK setup
193 KDL::ChainFkSolverPos_recursive fk(robot.chain);
194 KDL::JntArray q_min(n), q_max(n);
195 for (unsigned i = 0; i < n; ++i) {
196 int jid = model->dof_jntid[robot.kdl_to_mj_dof[i]];
197 if (model->jnt_limited[jid]) {
198 q_min(i) = model->jnt_range[2 * jid];
199 q_max(i) = model->jnt_range[2 * jid + 1];
200 } else {
201 q_min(i) = -2 * M_PI;
202 q_max(i) = 2 * M_PI;
203 }
204 }
205 KDL::ChainIkSolverVel_pinv ik_vel(robot.chain);
206 KDL::ChainIkSolverPos_NR_JL ik(robot.chain, q_min, q_max, fk, ik_vel, 500, 1e-5);
207
208 const double kGraspZ = kCubeZ + kGripperReach + 0.02;
209 const double kPreGraspZ = kGraspZ + 0.20;
210 const double kLiftZ = kGraspZ + 0.30;
211 const KDL::Rotation kDownRot = KDL::Rotation::Identity();
212
213 KDL::JntArray q_home(n), q_pregrasp(n), q_grasp(n), q_lift(n);
214 for (unsigned i = 0; i < n; ++i) q_home(i) = kHomePose[i];
215
216 struct WP
217 {
218 double z;
219 KDL::JntArray *out;
220 const KDL::JntArray *seed;
221 };
222 WP wps[] = {
223 { kPreGraspZ, &q_pregrasp, &q_home },
224 { kGraspZ, &q_grasp, &q_pregrasp },
225 { kLiftZ, &q_lift, &q_grasp },
226 };
227 for (auto &wp : wps) {
228 KDL::Frame tgt(kDownRot, KDL::Vector(kCubeX, kCubeY, wp.z));
229 if (ik.CartToJnt(*wp.seed, tgt, *wp.out) < 0)
230 std::cerr << "IK warning: did not converge for z=" << wp.z << "\n";
231 }
232
233 // state configuration table
234 /*
235 * HOLD duration is 1 s in headless mode (enough to verify the lift),
236 * and effectively infinite in GUI mode (runs until the window is closed).
237 */
238 const double kHoldDuration = headless ? 1.0 : 1e9;
239
240 // clang-format off
241 const StateConfig state_cfg[] = {
242 /* HOME */ { 1.0, 2.5, 0.08, &q_home, 0.0, PickState::PREGRASP },
243 /* PREGRASP*/ { 2.0, 4.0, 0.08, &q_pregrasp, 0.0, PickState::GRASP },
244 /* GRASP */ { 2.0, 4.0, 0.06, &q_grasp, 0.0, PickState::CLOSE },
245 /* CLOSE */ { 1.5, 2.5, -1.0, &q_grasp, 255.0, PickState::LIFT },
246 /* LIFT */ { 3.0, 5.0, 0.08, &q_lift, 255.0, PickState::HOLD },
247 /* HOLD */ { kHoldDuration, kHoldDuration, -1.0, &q_lift, 255.0, PickState::DONE },
248 /* DONE */ { 0.0, 0.0, -1.0, &q_lift, 255.0, PickState::DONE },
249 };
250 // clang-format on
251
252 auto cfg = [&](PickState s) -> const StateConfig & { return state_cfg[static_cast<int>(s)]; };
253
254 // initial conditions
255 auto reset_cube = [&](mjData *d) {
256 int qadr = model->jnt_qposadr[cube_jnt];
257 d->qpos[qadr + 0] = kCubeX;
258 d->qpos[qadr + 1] = kCubeY;
259 d->qpos[qadr + 2] = kCubeZ;
260 d->qpos[qadr + 3] = 1.0;
261 d->qpos[qadr + 4] = d->qpos[qadr + 5] = d->qpos[qadr + 6] = 0.0;
262 };
263
265
266 auto seed_reset_state = [&](mjData *d) {
267 for (unsigned i = 0; i < n; ++i) {
268 robot.jnt_pos_cmd[i] = d->qpos[robot.kdl_to_mj_qpos[i]];
269 robot.jnt_trq_cmd[i] = 0.0;
270 d->qfrc_applied[robot.kdl_to_mj_dof[i]] = 0.0;
271 }
272 mj_kdl::update(&robot);
273 if (fingers_act >= 0) d->ctrl[fingers_act] = 0.0;
274 };
275
276 auto reset_scene = [&](mjData *d) {
277 if (key_id >= 0) {
278 mj_resetDataKeyframe(model, d, key_id);
279 } else {
280 mj_resetData(model, d);
281 mj_kdl::set_joint_pos(&robot, q_home, false);
282 }
283 reset_cube(d);
284 mj_forward(model, d);
285 seed_reset_state(d);
286 };
287
288 reset_scene(data);
289
290 // GUI init (non-headless only)
291 mj_kdl::Viewer viewer;
292 if (!headless) {
293 if (!mj_kdl::init_window_sim(&viewer, &robot)) {
294 std::cerr << "init_window_sim() failed\n";
295 mj_kdl::cleanup(&robot);
296 mj_kdl::destroy_scene(model, data);
297 return 1;
298 }
299 }
300
301 StateMachine sm;
302 sm.q_enter = KDL::JntArray(n);
303
304 KDL::JntArray q_des(n);
305
306 auto begin_state = [&](PickState next) {
307 sm.state = next;
308 sm.t_enter = data->time;
309 snapshot_q(robot, n, sm.q_enter);
310 };
311
312 begin_state(PickState::HOME);
313
314 double prev_sim_time = data->time;
315
316 while (sm.state != PickState::DONE) {
317 switch (sm.state) {
318 case PickState::HOME:
319 LOG_INFO("State: HOME");
320 while (sm.state == PickState::HOME) {
321 if (data->time < prev_sim_time - 1e-6) {
322 reset_scene(data);
323 begin_state(PickState::HOME);
324 prev_sim_time = data->time;
325 continue;
326 }
327 prev_sim_time = data->time;
328
329 const StateConfig &state = cfg(sm.state);
330 double alpha = (state.duration > 0.0)
331 ? clamp01((data->time - sm.t_enter) / state.duration)
332 : 1.0;
333 lerp_q(sm.q_enter, *state.q_target, alpha, q_des);
334
335 // HOME uses the default joint-impedance controller.
336 impedance_ctrl(robot, q_des, n, dyn);
337 mj_kdl::update(&robot);
338 if (fingers_act >= 0) data->ctrl[fingers_act] = state.gripper_cmd;
339
340 double t_rel = data->time - sm.t_enter;
341 double pose_err = max_abs_joint_err(robot, *state.q_target, n);
342 bool done_time = t_rel >= state.duration;
343 bool done_pose = (state.settle_tol < 0.0) || (pose_err <= state.settle_tol);
344 bool done_timeout = (state.timeout > 0.0) && (t_rel >= state.timeout);
345 if ((done_time && done_pose) || done_timeout) {
346 begin_state(state.next);
347 break;
348 }
349
350 if (headless) {
351 mj_kdl::step(&robot);
352 } else if (!mj_kdl::tick(&viewer, model, data)) {
354 break;
355 }
356 }
357 break;
358
360 LOG_INFO("State: PREGRASP");
361 while (sm.state == PickState::PREGRASP) {
362 if (data->time < prev_sim_time - 1e-6) {
363 reset_scene(data);
364 begin_state(PickState::HOME);
365 prev_sim_time = data->time;
366 break;
367 }
368 prev_sim_time = data->time;
369
370 const StateConfig &state = cfg(sm.state);
371 double alpha = (state.duration > 0.0)
372 ? clamp01((data->time - sm.t_enter) / state.duration)
373 : 1.0;
374 lerp_q(sm.q_enter, *state.q_target, alpha, q_des);
375
376 // PREGRASP could swap in a different approach controller later.
377 impedance_ctrl(robot, q_des, n, dyn);
378 mj_kdl::update(&robot);
379 if (fingers_act >= 0) data->ctrl[fingers_act] = state.gripper_cmd;
380
381 double t_rel = data->time - sm.t_enter;
382 double pose_err = max_abs_joint_err(robot, *state.q_target, n);
383 bool done_time = t_rel >= state.duration;
384 bool done_pose = (state.settle_tol < 0.0) || (pose_err <= state.settle_tol);
385 bool done_timeout = (state.timeout > 0.0) && (t_rel >= state.timeout);
386 if ((done_time && done_pose) || done_timeout) {
387 begin_state(state.next);
388 break;
389 }
390
391 if (headless) {
392 mj_kdl::step(&robot);
393 } else if (!mj_kdl::tick(&viewer, model, data)) {
395 break;
396 }
397 }
398 break;
399
400 case PickState::GRASP:
401 LOG_INFO("State: GRASP");
402 while (sm.state == PickState::GRASP) {
403 if (data->time < prev_sim_time - 1e-6) {
404 reset_scene(data);
405 begin_state(PickState::HOME);
406 prev_sim_time = data->time;
407 break;
408 }
409 prev_sim_time = data->time;
410
411 const StateConfig &state = cfg(sm.state);
412 double alpha = (state.duration > 0.0)
413 ? clamp01((data->time - sm.t_enter) / state.duration)
414 : 1.0;
415 lerp_q(sm.q_enter, *state.q_target, alpha, q_des);
416
417 // GRASP keeps the same structure but can use contact-aware logic later.
418 impedance_ctrl(robot, q_des, n, dyn);
419 mj_kdl::update(&robot);
420 if (fingers_act >= 0) data->ctrl[fingers_act] = state.gripper_cmd;
421
422 double t_rel = data->time - sm.t_enter;
423 double pose_err = max_abs_joint_err(robot, *state.q_target, n);
424 bool done_time = t_rel >= state.duration;
425 bool done_pose = (state.settle_tol < 0.0) || (pose_err <= state.settle_tol);
426 bool done_timeout = (state.timeout > 0.0) && (t_rel >= state.timeout);
427 if ((done_time && done_pose) || done_timeout) {
428 begin_state(state.next);
429 break;
430 }
431
432 if (headless) {
433 mj_kdl::step(&robot);
434 } else if (!mj_kdl::tick(&viewer, model, data)) {
436 break;
437 }
438 }
439 break;
440
441 case PickState::CLOSE:
442 LOG_INFO("State: CLOSE");
443 while (sm.state == PickState::CLOSE) {
444 if (data->time < prev_sim_time - 1e-6) {
445 reset_scene(data);
446 begin_state(PickState::HOME);
447 prev_sim_time = data->time;
448 break;
449 }
450 prev_sim_time = data->time;
451
452 const StateConfig &state = cfg(sm.state);
453 double alpha = (state.duration > 0.0)
454 ? clamp01((data->time - sm.t_enter) / state.duration)
455 : 1.0;
456 lerp_q(sm.q_enter, *state.q_target, alpha, q_des);
457
458 // CLOSE could switch to a grasp-force controller instead of pure impedance.
459 impedance_ctrl(robot, q_des, n, dyn);
460 mj_kdl::update(&robot);
461 if (fingers_act >= 0) data->ctrl[fingers_act] = state.gripper_cmd;
462
463 double t_rel = data->time - sm.t_enter;
464 double pose_err = max_abs_joint_err(robot, *state.q_target, n);
465 bool done_time = t_rel >= state.duration;
466 bool done_pose = (state.settle_tol < 0.0) || (pose_err <= state.settle_tol);
467 bool done_timeout = (state.timeout > 0.0) && (t_rel >= state.timeout);
468 if ((done_time && done_pose) || done_timeout) {
469 begin_state(state.next);
470 break;
471 }
472
473 if (headless) {
474 mj_kdl::step(&robot);
475 } else if (!mj_kdl::tick(&viewer, model, data)) {
477 break;
478 }
479 }
480 break;
481
482 case PickState::LIFT:
483 LOG_INFO("State: LIFT");
484 while (sm.state == PickState::LIFT) {
485 if (data->time < prev_sim_time - 1e-6) {
486 reset_scene(data);
487 begin_state(PickState::HOME);
488 prev_sim_time = data->time;
489 break;
490 }
491 prev_sim_time = data->time;
492
493 const StateConfig &state = cfg(sm.state);
494 double alpha = (state.duration > 0.0)
495 ? clamp01((data->time - sm.t_enter) / state.duration)
496 : 1.0;
497 lerp_q(sm.q_enter, *state.q_target, alpha, q_des);
498
499 // LIFT could use a stiffer transport controller if needed.
500 impedance_ctrl(robot, q_des, n, dyn);
501 mj_kdl::update(&robot);
502 if (fingers_act >= 0) data->ctrl[fingers_act] = state.gripper_cmd;
503
504 double t_rel = data->time - sm.t_enter;
505 double pose_err = max_abs_joint_err(robot, *state.q_target, n);
506 bool done_time = t_rel >= state.duration;
507 bool done_pose = (state.settle_tol < 0.0) || (pose_err <= state.settle_tol);
508 bool done_timeout = (state.timeout > 0.0) && (t_rel >= state.timeout);
509 if ((done_time && done_pose) || done_timeout) {
510 begin_state(state.next);
511 break;
512 }
513
514 if (headless) {
515 mj_kdl::step(&robot);
516 } else if (!mj_kdl::tick(&viewer, model, data)) {
518 break;
519 }
520 }
521 break;
522
523 case PickState::HOLD:
524 LOG_INFO("State: HOLD");
525 while (sm.state == PickState::HOLD) {
526 if (data->time < prev_sim_time - 1e-6) {
527 reset_scene(data);
528 begin_state(PickState::HOME);
529 prev_sim_time = data->time;
530 break;
531 }
532 prev_sim_time = data->time;
533
534 const StateConfig &state = cfg(sm.state);
535 double alpha = (state.duration > 0.0)
536 ? clamp01((data->time - sm.t_enter) / state.duration)
537 : 1.0;
538 lerp_q(sm.q_enter, *state.q_target, alpha, q_des);
539
540 // HOLD can later become a dedicated grasp-maintenance controller.
541 impedance_ctrl(robot, q_des, n, dyn);
542 mj_kdl::update(&robot);
543 if (fingers_act >= 0) data->ctrl[fingers_act] = state.gripper_cmd;
544
545 double t_rel = data->time - sm.t_enter;
546 double pose_err = max_abs_joint_err(robot, *state.q_target, n);
547 bool done_time = t_rel >= state.duration;
548 bool done_pose = (state.settle_tol < 0.0) || (pose_err <= state.settle_tol);
549 bool done_timeout = (state.timeout > 0.0) && (t_rel >= state.timeout);
550 if ((done_time && done_pose) || done_timeout) {
551 begin_state(state.next);
552 break;
553 }
554
555 if (headless) {
556 mj_kdl::step(&robot);
557 } else if (!mj_kdl::tick(&viewer, model, data)) {
559 break;
560 }
561 }
562 break;
563
564 case PickState::DONE:
565 break;
566 }
567 }
568
569 if (headless) {
570 int qadr = model->jnt_qposadr[cube_jnt];
571 double cube_z = data->qpos[qadr + 2];
572 std::cout << "cube Z after pick: " << std::fixed << std::setprecision(3) << cube_z
573 << " m\n";
574 } else {
575 mj_kdl::cleanup(&viewer);
576 }
577
578 mj_kdl::cleanup(&robot);
579 mj_kdl::destroy_scene(model, data);
580 return 0;
581}
int main(int argc, char *argv[])
Definition ex_pick.cpp:120
PickState
Definition ex_pick.cpp:92
void step(Robot *s)
bool init_robot_from_mjcf(Robot *r, mjModel *model, mjData *data, const char *base_body, const char *tip_body, const char *prefix="", const char *tool_body=nullptr)
void cleanup(Robot *r)
void set_joint_pos(Robot *r, const KDL::JntArray &q, bool call_forward=true)
void update(Robot *r)
bool build_scene(mjModel **out_model, mjData **out_data, const SceneSpec *spec)
void destroy_scene(mjModel *model, mjData *data)
bool init_window_sim(Viewer *v, Robot *r, const char *title="MuJoCo")
bool tick(Viewer *v, Robot *r)
#define LOG_INFO(expr)
double timeout
Definition ex_pick.cpp:101
const KDL::JntArray * q_target
Definition ex_pick.cpp:103
double settle_tol
Definition ex_pick.cpp:102
PickState next
Definition ex_pick.cpp:105
double gripper_cmd
Definition ex_pick.cpp:104
double duration
Definition ex_pick.cpp:100
double t_enter
Definition ex_pick.cpp:116
KDL::JntArray q_enter
Definition ex_pick.cpp:117
PickState state
Definition ex_pick.cpp:115
std::vector< AttachmentSpec > attachments
std::vector< int > kdl_to_mj_qpos
std::vector< int > kdl_to_mj_dof
std::vector< double > jnt_pos_msr
std::vector< double > jnt_pos_cmd
std::vector< double > jnt_vel_msr
std::vector< double > jnt_trq_cmd
std::vector< RobotSpec > robots
std::vector< SceneObject > objects