123int main(
int argc,
char *argv[])
125 bool headless =
false;
126 bool do_record =
false;
127 std::string record_path =
"table_pour.mp4";
128 for (
int i = 1; i < argc; ++i) {
129 const std::string arg = argv[i];
130 if (arg ==
"--headless") {
132 }
else if (arg ==
"--record") {
135 if (i + 1 < argc && argv[i + 1][0] !=
'-') record_path = argv[++i];
138 const int num_balls = headless ? kNumBallsHeadless : kNumBallsGui;
140 const fs::path root = repo_root();
141 if (!fs::exists(root /
"third_party/menagerie")) {
142 std::cerr <<
"third_party/menagerie/ not found - run:\n"
143 " cmake -B build -DFETCH_MENAGERIE=ON\n";
147 const std::string arm_mjcf = (root /
"third_party/menagerie/kinova_gen3/gen3.xml").
string();
148 const std::string grp_mjcf = (root /
"third_party/menagerie/robotiq_2f85/2f85.xml").
string();
149 const std::string bottle_mjcf = (root /
"src/examples/assets/mug.xml").
string();
150 const std::string receiver_mjcf = (root /
"src/examples/assets/mug_table.xml").
string();
151 const std::string table_mjcf = (root /
"src/examples/assets/table.xml").
string();
157 gripper.
pos[2] = -0.061525;
158 gripper.
euler[0] = 180.0;
169 robot_spec.
path = arm_mjcf.c_str();
170 robot_spec.
pos[0] = kRobotBackX;
171 robot_spec.
pos[2] = kTableZ;
178 .mjcf_path = table_mjcf,
179 .pos = { 0.0, 0.0, kTableZ },
182 scene_cfg.
objects.push_back(table);
183 for (
int i = 0; i < num_balls; ++i) scene_cfg.
objects.push_back(make_ball(i));
186 .mjcf_path = receiver_mjcf,
187 .pos = { kJugX, kJugY, kReceiverFrameZ },
189 scene_cfg.
robots.push_back(robot_spec);
191 mjModel *model =
nullptr;
192 mjData *data =
nullptr;
194 std::cerr <<
"build_scene() failed\n";
198 KDL::Frame world_T_table_top;
201 std::cerr <<
"table_top site not found\n";
212 &robot, model, data,
"base_link",
"bracelet_link",
"", &tool
214 std::cerr <<
"init_robot_from_mjcf() failed\n";
219 const unsigned n = robot.
chain.getNrOfJoints();
220 const int fingers_act = mj_name2id(model, mjOBJ_ACTUATOR,
"g_fingers_actuator");
221 if (fingers_act < 0) {
222 std::cerr <<
"g_fingers_actuator not found\n";
228 KDL::JntArray q_home(n);
229 for (
unsigned i = 0; i < n; ++i) q_home(i) = kHomePose[i];
231 KDL::ChainFkSolverPos_recursive fk(robot.
chain);
232 KDL::JntArray q_min(n), q_max(n);
233 for (
unsigned i = 0; i < n; ++i) {
235 if (model->jnt_limited[jid]) {
236 q_min(i) = model->jnt_range[2 * jid];
237 q_max(i) = model->jnt_range[2 * jid + 1];
239 q_min(i) = -2 * M_PI;
243 KDL::ChainIkSolverVel_pinv ik_vel(robot.
chain);
244 KDL::ChainIkSolverPos_NR_JL ik_nr(robot.
chain, q_min, q_max, fk, ik_vel, 2000, 1e-5);
245 KDL::ChainIkSolverPos_LMA ik_lma(robot.
chain, 1e-5, 2000);
246 KDL::ChainDynParam dyn(robot.
chain, KDL::Vector(0.0, 0.0, scene_cfg.
gravity_z));
249 fk.JntToCart(q_home, home_fk);
250 const KDL::Rotation carry_tcp = home_fk.M * KDL::Rotation::RotY(-0.05);
252 KDL::JntArray q_pre_pour(n), q_pour(n), q_tilt(n), q_retreat(n);
258 const KDL::JntArray *seed;
260 const KDL::Frame world_T_base(
261 KDL::Rotation::Identity(), KDL::Vector(kRobotBackX, 0.0, kTableZ)
263 const KDL::Frame base_T_world = world_T_base.Inverse();
266 KDL::Frame world_T_outlet, world_T_tcp;
269 const KDL::Vector tcp_outlet = world_T_tcp.Inverse() * world_T_outlet.p;
271 const auto outlet_target_to_tcp_target =
272 [&](
const KDL::Rotation &tcp_rot,
const KDL::Vector &outlet_pos) {
273 return KDL::Frame(tcp_rot, outlet_pos - tcp_rot * tcp_outlet);
276 std::array<KDL::Vector, 3> waypoint_pos = {
277 KDL::Vector(kJugX, kJugY, kTableZ + 0.27),
278 KDL::Vector(kJugX, kJugY, kTableZ + 0.20),
279 KDL::Vector(kRetreatX, kRetreatY, kTableZ + 0.27),
281 const auto solve_waypoints = [&](
const std::array<KDL::Vector, 3> &pos) {
282 Waypoint waypoints[] = {
284 base_T_world * outlet_target_to_tcp_target(carry_tcp, pos[0]),
288 base_T_world * outlet_target_to_tcp_target(carry_tcp, pos[1]),
292 base_T_world * outlet_target_to_tcp_target(carry_tcp, pos[2]),
296 for (
const auto &wp : waypoints) {
297 bool ok = ik_nr.CartToJnt(*wp.seed, wp.target, *wp.out) >= 0;
298 if (!ok) ok = ik_lma.CartToJnt(*wp.seed, wp.target, *wp.out) >= 0;
300 std::cerr <<
"IK failed for " << wp.name <<
"\n";
304 fk.JntToCart(*wp.out, fk_out);
305 if ((wp.target.p - fk_out.p).Norm() > kIkTol) {
306 std::cerr <<
"IK pose error for " << wp.name <<
"\n";
312 if (!solve_waypoints(waypoint_pos)) {
318 q_tilt(n - 1) += kPourTiltRad;
319 for (
int iter = 0; iter < 4; ++iter) {
322 const double dx = kJugX - world_T_outlet.p.x();
323 const double dy = kJugY - world_T_outlet.p.y();
324 const double dz = kTiltOutletZ - world_T_outlet.p.z();
325 if (std::sqrt(dx * dx + dy * dy + dz * dz) < 5e-3)
break;
327 waypoint_pos[1][0] += dx;
328 waypoint_pos[1][1] += dy;
329 waypoint_pos[1][2] += dz;
330 if (!solve_waypoints(waypoint_pos)) {
336 q_tilt(n - 1) += kPourTiltRad;
339 std::vector<int> grain_joints;
340 grain_joints.reserve(num_balls);
341 for (
int i = 0; i < num_balls; ++i) {
343 std::snprintf(name,
sizeof(name),
"grain_%02d_joint", i);
344 int jid = mj_name2id(model, mjOBJ_JOINT, name);
345 if (jid >= 0) grain_joints.push_back(jid);
358 mjModel *m = ctx->model;
359 mjData *d = ctx->data;
362 KDL::Frame world_T_center;
365 const double spacing = 2.00 * kBallRadius;
366 for (
int i = 0; i < num_balls; ++i) {
367 const int layer = i / 9;
368 const int slot = i % 9;
369 const double ix =
static_cast<double>(slot % 3) - 1.0;
370 const double iy =
static_cast<double>(slot / 3) - 1.0;
371 KDL::Vector world_v =
372 world_T_center * KDL::Vector(ix * spacing, iy * spacing, -0.026 + layer * spacing);
373 const double world[3] = { world_v.x(), world_v.y(), world_v.z() };
375 std::snprintf(body_name,
sizeof(body_name),
"grain_%02d", i);
378 d->ctrl[fingers_act] = 255.0;
381 double prev_sim_time = 0.0;
382 bool restart =
false;
383 bool aborted =
false;
385 auto reset_scene = [&]() {
387 prev_sim_time = data->time;
391 const std::vector<Phase> phases = {
397 .gripper_cmd = 255.0 },
398 { .name =
"PRE_POUR",
399 .target = &q_pre_pour,
403 .gripper_cmd = 255.0 },
409 .gripper_cmd = 255.0 },
415 .gripper_cmd = 255.0 },
416 { .name =
"POUR_HOLD",
418 .duration = headless ? 9.0 : 10.0,
419 .timeout = headless ? 10.0 : 11.0,
421 .gripper_cmd = 255.0 },
423 .target = &q_retreat,
427 .gripper_cmd = 255.0 },
429 .target = &q_retreat,
430 .duration = headless ? 1.0 : 1e9,
431 .timeout = headless ? 1.0 : 1e9,
433 .gripper_cmd = 255.0 },
437 bool recorder_ok =
false;
438 const int kRecordFps = 60;
439 const int steps_per_frame =
440 std::max(1,
static_cast<int>(1.0 / (kRecordFps * model->opt.timestep)));
446 std::cerr <<
"init_video_recorder() failed -- is EGL available and ffmpeg installed?\n";
451 recorder.
cam.azimuth = 145.0;
452 recorder.
cam.elevation = -22.0;
453 recorder.
cam.distance = 1.35;
454 recorder.
cam.lookat[0] = 0.05;
455 recorder.
cam.lookat[1] = 0.02;
456 recorder.
cam.lookat[2] = 0.88;
462 std::cerr <<
"init_window_sim() failed\n";
470 KDL::JntArray q_enter(n), q_des(n);
473 for (
const Phase &phase : phases) {
475 std::cout <<
"State: " << phase.name <<
"\n";
476 const double t_enter = data->time;
477 snapshot_q(robot, n, q_enter);
479 if (data->time < prev_sim_time - 1e-6) {
483 prev_sim_time = data->time;
487 phase.duration > 0.0 ? clamp01((data->time - t_enter) / phase.duration) : 1.0;
488 lerp_q(q_enter, *phase.target, alpha, q_des);
489 impedance_ctrl(robot, q_des, n, dyn);
490 data->ctrl[fingers_act] = phase.gripper_cmd;
493 const double t_rel = data->time - t_enter;
494 const bool done_time = t_rel >= phase.duration;
495 const bool done_pose =
496 phase.settle_tol < 0.0
497 || max_abs_joint_err(robot, *phase.target, n) <= phase.settle_tol;
498 const bool done_timeout = phase.timeout > 0.0 && t_rel >= phase.timeout;
499 if ((done_time && done_pose) || done_timeout)
break;
506 if (recorder_ok && sim_step % steps_per_frame == 0) {
508 std::cerr <<
"record_frame() failed at step " << sim_step <<
"\n";
520 std::cout <<
"Saved recording: " << record_path <<
"\n";
527 for (
int jid : grain_joints)
528 if (inside_jug(data, model, jid)) ++in_jug;
529 for (
int jid : grain_joints) {
530 const double *p = data->qpos + model->jnt_qposadr[jid];
535 if (!grain_joints.empty()) {
536 avg[0] /=
static_cast<double>(grain_joints.size());
537 avg[1] /=
static_cast<double>(grain_joints.size());
538 avg[2] /=
static_cast<double>(grain_joints.size());
541 std::cout <<
"balls in transparent receiver: " << in_jug <<
"/" << grain_joints.size()
543 std::cout <<
"grain centroid: [" << std::fixed << std::setprecision(3) << avg[0] <<
", "
544 << avg[1] <<
", " << avg[2] <<
"] receiver center=[" << kJugX <<
", " << kJugY
546 if (headless && in_jug < 4) {
547 std::cerr <<
"pour failed: too few balls reached the receiver\n";