120int main(
int argc,
char *argv[])
122 bool headless =
false;
123 bool do_record =
false;
124 std::string record_path =
"table_pour.mp4";
125 for (
int i = 1; i < argc; ++i) {
126 const std::string arg = argv[i];
127 if (arg ==
"--headless") {
129 }
else if (arg ==
"--record") {
132 if (i + 1 < argc && argv[i + 1][0] !=
'-') record_path = argv[++i];
135 const int num_balls = headless ? kNumBallsHeadless : kNumBallsGui;
157 robot_spec.
path = arm_mjcf.c_str();
158 robot_spec.
pos[0] = kRobotBackX;
159 robot_spec.
pos[2] = kTableZ;
169 .mjcf_path = table_mjcf,
170 .pos = { 0.0, 0.0, kTableZ },
173 scene_cfg.
objects.push_back(table);
174 for (
int i = 0; i < num_balls; ++i) scene_cfg.
objects.push_back(make_ball(i));
177 .mjcf_path = receiver_mjcf,
178 .pos = { kJugX, kJugY, kReceiverFrameZ },
180 scene_cfg.
robots.push_back(robot_spec);
182 mjModel *model =
nullptr;
183 mjData *data =
nullptr;
185 std::cerr <<
"build_scene() failed\n";
189 KDL::Frame world_T_table_top;
192 std::cerr <<
"table_top site not found\n";
203 &robot, model, data,
"base_link",
"bracelet_link",
"", &tool
205 std::cerr <<
"init_robot_from_mjcf() failed\n";
210 const unsigned n = robot.
chain.getNrOfJoints();
211 const int fingers_act = mj_name2id(model, mjOBJ_ACTUATOR,
"g_fingers_actuator");
212 if (fingers_act < 0) {
213 std::cerr <<
"g_fingers_actuator not found\n";
219 KDL::JntArray q_home(n);
220 for (
unsigned i = 0; i < n; ++i) q_home(i) = kHomePose[i];
222 KDL::ChainFkSolverPos_recursive fk(robot.
chain);
223 KDL::JntArray q_min(n), q_max(n);
224 for (
unsigned i = 0; i < n; ++i) {
226 if (model->jnt_limited[jid]) {
227 q_min(i) = model->jnt_range[2 * jid];
228 q_max(i) = model->jnt_range[2 * jid + 1];
230 q_min(i) = -2 * M_PI;
234 KDL::ChainIkSolverVel_pinv ik_vel(robot.
chain);
235 KDL::ChainIkSolverPos_NR_JL ik_nr(robot.
chain, q_min, q_max, fk, ik_vel, 2000, 1e-5);
236 KDL::ChainIkSolverPos_LMA ik_lma(robot.
chain, 1e-5, 2000);
237 KDL::ChainDynParam dyn(robot.
chain, KDL::Vector(0.0, 0.0, scene_cfg.
gravity_z));
240 fk.JntToCart(q_home, home_fk);
241 const KDL::Rotation carry_tcp = home_fk.M * KDL::Rotation::RotY(-0.05);
243 KDL::JntArray q_pre_pour(n), q_pour(n), q_tilt(n), q_retreat(n);
249 const KDL::JntArray *seed;
251 const KDL::Frame world_T_base(
252 KDL::Rotation::Identity(), KDL::Vector(kRobotBackX, 0.0, kTableZ)
254 const KDL::Frame base_T_world = world_T_base.Inverse();
257 KDL::Frame world_T_outlet, world_T_tcp;
260 const KDL::Vector tcp_outlet = world_T_tcp.Inverse() * world_T_outlet.p;
262 const auto outlet_target_to_tcp_target =
263 [&](
const KDL::Rotation &tcp_rot,
const KDL::Vector &outlet_pos) {
264 return KDL::Frame(tcp_rot, outlet_pos - tcp_rot * tcp_outlet);
267 std::array<KDL::Vector, 3> waypoint_pos = {
268 KDL::Vector(kJugX, kJugY, kTableZ + 0.27),
269 KDL::Vector(kJugX, kJugY, kTableZ + 0.20),
270 KDL::Vector(kRetreatX, kRetreatY, kTableZ + 0.27),
272 const auto solve_waypoints = [&](
const std::array<KDL::Vector, 3> &pos) {
273 Waypoint waypoints[] = {
275 base_T_world * outlet_target_to_tcp_target(carry_tcp, pos[0]),
279 base_T_world * outlet_target_to_tcp_target(carry_tcp, pos[1]),
283 base_T_world * outlet_target_to_tcp_target(carry_tcp, pos[2]),
287 for (
const auto &wp : waypoints) {
288 bool ok = ik_nr.CartToJnt(*wp.seed, wp.target, *wp.out) >= 0;
289 if (!ok) ok = ik_lma.CartToJnt(*wp.seed, wp.target, *wp.out) >= 0;
291 std::cerr <<
"IK failed for " << wp.name <<
"\n";
295 fk.JntToCart(*wp.out, fk_out);
296 if ((wp.target.p - fk_out.p).Norm() > kIkTol) {
297 std::cerr <<
"IK pose error for " << wp.name <<
"\n";
303 if (!solve_waypoints(waypoint_pos)) {
309 q_tilt(n - 1) += kPourTiltRad;
310 for (
int iter = 0; iter < 4; ++iter) {
313 const double dx = kJugX - world_T_outlet.p.x();
314 const double dy = kJugY - world_T_outlet.p.y();
315 const double dz = kTiltOutletZ - world_T_outlet.p.z();
316 if (std::sqrt(dx * dx + dy * dy + dz * dz) < 5e-3)
break;
318 waypoint_pos[1][0] += dx;
319 waypoint_pos[1][1] += dy;
320 waypoint_pos[1][2] += dz;
321 if (!solve_waypoints(waypoint_pos)) {
327 q_tilt(n - 1) += kPourTiltRad;
330 std::vector<int> grain_joints;
331 grain_joints.reserve(num_balls);
332 for (
int i = 0; i < num_balls; ++i) {
334 std::snprintf(name,
sizeof(name),
"grain_%02d_joint", i);
335 int jid = mj_name2id(model, mjOBJ_JOINT, name);
336 if (jid >= 0) grain_joints.push_back(jid);
349 mjModel *m = ctx->model;
350 mjData *d = ctx->data;
353 KDL::Frame world_T_center;
356 const double spacing = 2.00 * kBallRadius;
357 for (
int i = 0; i < num_balls; ++i) {
358 const int layer = i / 9;
359 const int slot = i % 9;
360 const double ix =
static_cast<double>(slot % 3) - 1.0;
361 const double iy =
static_cast<double>(slot / 3) - 1.0;
362 KDL::Vector world_v =
363 world_T_center * KDL::Vector(ix * spacing, iy * spacing, -0.026 + layer * spacing);
364 const double world[3] = { world_v.x(), world_v.y(), world_v.z() };
366 std::snprintf(body_name,
sizeof(body_name),
"grain_%02d", i);
369 d->ctrl[fingers_act] = 0.8;
372 double prev_sim_time = 0.0;
373 bool restart =
false;
374 bool aborted =
false;
376 auto reset_scene = [&]() {
378 prev_sim_time = data->time;
382 const std::vector<Phase> phases = {
388 .gripper_cmd = 0.8 },
389 { .name =
"PRE_POUR",
390 .target = &q_pre_pour,
394 .gripper_cmd = 0.8 },
400 .gripper_cmd = 0.8 },
406 .gripper_cmd = 0.8 },
407 { .name =
"POUR_HOLD",
409 .duration = headless ? 9.0 : 10.0,
410 .timeout = headless ? 10.0 : 11.0,
412 .gripper_cmd = 0.8 },
414 .target = &q_retreat,
418 .gripper_cmd = 0.8 },
420 .target = &q_retreat,
421 .duration = headless ? 1.0 : 1e9,
422 .timeout = headless ? 1.0 : 1e9,
424 .gripper_cmd = 0.8 },
428 bool recorder_ok =
false;
429 const int kRecordFps = 60;
430 const int steps_per_frame =
431 std::max(1,
static_cast<int>(1.0 / (kRecordFps * model->opt.timestep)));
437 std::cerr <<
"init_video_recorder() failed -- is EGL available and ffmpeg installed?\n";
442 recorder.
cam.azimuth = 145.0;
443 recorder.
cam.elevation = -22.0;
444 recorder.
cam.distance = 1.35;
445 recorder.
cam.lookat[0] = 0.05;
446 recorder.
cam.lookat[1] = 0.02;
447 recorder.
cam.lookat[2] = 0.88;
453 std::cerr <<
"init_window_sim() failed\n";
461 KDL::JntArray q_enter(n), q_des(n);
464 for (
const Phase &phase : phases) {
466 std::cout <<
"State: " << phase.name <<
"\n";
467 const double t_enter = data->time;
468 snapshot_q(robot, n, q_enter);
470 if (data->time < prev_sim_time - 1e-6) {
474 prev_sim_time = data->time;
478 phase.duration > 0.0 ? clamp01((data->time - t_enter) / phase.duration) : 1.0;
479 lerp_q(q_enter, *phase.target, alpha, q_des);
480 impedance_ctrl(robot, q_des, n, dyn);
481 data->ctrl[fingers_act] = phase.gripper_cmd;
484 const double t_rel = data->time - t_enter;
485 const bool done_time = t_rel >= phase.duration;
486 const bool done_pose =
487 phase.settle_tol < 0.0
488 || max_abs_joint_err(robot, *phase.target, n) <= phase.settle_tol;
489 const bool done_timeout = phase.timeout > 0.0 && t_rel >= phase.timeout;
490 if ((done_time && done_pose) || done_timeout)
break;
497 if (recorder_ok && sim_step % steps_per_frame == 0) {
499 std::cerr <<
"record_frame() failed at step " << sim_step <<
"\n";
511 std::cout <<
"Saved recording: " << record_path <<
"\n";
518 for (
int jid : grain_joints)
519 if (inside_jug(data, model, jid)) ++in_jug;
520 for (
int jid : grain_joints) {
521 const double *p = data->qpos + model->jnt_qposadr[jid];
526 if (!grain_joints.empty()) {
527 avg[0] /=
static_cast<double>(grain_joints.size());
528 avg[1] /=
static_cast<double>(grain_joints.size());
529 avg[2] /=
static_cast<double>(grain_joints.size());
532 std::cout <<
"balls in transparent receiver: " << in_jug <<
"/" << grain_joints.size()
534 std::cout <<
"grain centroid: [" << std::fixed << std::setprecision(3) << avg[0] <<
", "
535 << avg[1] <<
", " << avg[2] <<
"] receiver center=[" << kJugX <<
", " << kJugY
537 if (headless && in_jug < 4) {
538 std::cerr <<
"pour failed: too few balls reached the receiver\n";