/** * @file hand_bridge.cpp * @brief Local TCP <-> DDS bridge for the web dashboard. * - "v0 v1 ... v11\n" -> publish MotorCmds_ to rt/inspire/cmd (q in [0,1], or <0 = release) * - "R\n" -> reply with the 12 latest rt/inspire/state q values (read-back / capture) * - "S\n" -> reply with 24 values: 12 angles then 12 forces (joint tracker) * Persistent publisher + state subscriber. Needs ./inspire_g1 running. * * ./hand_bridge [iface] [port] (default eth0 7799) */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include int main(int argc, char **argv) { std::string iface = argc > 1 ? argv[1] : "eth0"; int port = argc > 2 ? std::atoi(argv[2]) : 7799; unitree::robot::ChannelFactory::Instance()->Init(0, iface); auto pub = std::make_shared>("rt/inspire/cmd"); pub->InitChannel(); unitree_go::msg::dds_::MotorCmds_ cmd; cmd.cmds().resize(12); // Subscribe to hand state for read-back / capture. std::mutex mtx; double state[12] = {0}; double force[12] = {0}; double cur[12] = {0}, fset[12] = {0}; // dq = current mA, ddq = force limit double temp[12] = {0}, err[12] = {0}, sta[12] = {0}, lost[12] = {0}; double rate = 0; int rn = 0; auto rt0 = std::chrono::steady_clock::now(); // ---- force-follow (admittance) -------------------------------------------------- // The RH56 cannot be back-driven, so pushing a finger moves it 0.000. This closes the // loop in software: read the press, drive the finger that way, hold when you stop. It // lives here rather than in the web layer because the loop needs the 30Hz state stream // and the command publisher in the same place -- polling over HTTP is far too slow. std::atomic follow{false}; std::atomic fgain{0.0006}, fdead{60.0}, fmaxrate{0.35}, fforce{400.0}; double ftarget[12] = {0}, fbase[12] = {0}; std::atomic fcal{false}; // tau_est, in grams — the joint tracker needs BOTH angle and // force, because 'force moved but angle did not' is the whole // signature of a finger that cannot be back-driven. auto sub = std::make_shared>("rt/inspire/state"); sub->InitChannel([&](const void *m) { auto s = (const unitree_go::msg::dds_::MotorStates_ *)m; std::lock_guard lk(mtx); for (int i = 0; i < 12 && i < (int)s->states().size(); i++) { state[i] = s->states()[i].q(); force[i] = s->states()[i].tau_est(); cur[i] = s->states()[i].dq(); // mA fset[i] = s->states()[i].ddq(); // grip force limit temp[i] = s->states()[i].temperature(); err[i] = s->states()[i].reserve()[0]; sta[i] = s->states()[i].reserve()[1]; lost[i] = s->states()[i].lost(); } // Measure the ACTUAL publish rate here — a hand whose bus has died keeps serving // cached values, so a healthy-looking readout with a collapsed rate is the tell. if (++rn >= 20) { auto now = std::chrono::steady_clock::now(); double dt = std::chrono::duration(now - rt0).count(); if (dt > 0) rate = rn / dt; rn = 0; rt0 = now; } }); // Admittance thread: only publishes while follow is on, so it never fights the // dashboard, the recorder, or anything else driving the hand. std::thread ftick([&]{ auto prev = std::chrono::steady_clock::now(); while (true) { std::this_thread::sleep_for(std::chrono::milliseconds(33)); auto now = std::chrono::steady_clock::now(); double dt = std::chrono::duration(now - prev).count(); prev = now; if (!follow.load()) continue; if (dt > 0.2) dt = 0.2; { std::lock_guard lk(mtx); if (fcal.exchange(false)) { // arm: zero on the current force for (int i = 0; i < 12; i++) { fbase[i] = force[i]; ftarget[i] = state[i]; } } for (int i = 0; i < 12; i++) { double dev = force[i] - fbase[i]; if (std::abs(dev) <= fdead.load()) continue; dev -= (dev > 0 ? fdead.load() : -fdead.load()); double step = -fgain.load() * dev * dt; // press pad (+force) -> close double cap = fmaxrate.load() * dt; if (step > cap) step = cap; if (step < -cap) step = -cap; ftarget[i] += step; if (ftarget[i] < 0.0) ftarget[i] = 0.0; if (ftarget[i] > 1.0) ftarget[i] = 1.0; } for (int i = 0; i < 12; i++) { cmd.cmds()[i].q() = ftarget[i]; cmd.cmds()[i].kp() = fforce.load(); } } pub->Write(cmd); } }); ftick.detach(); int srv = socket(AF_INET, SOCK_STREAM, 0); int opt = 1; setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); sockaddr_in addr{}; addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr("127.0.0.1"); addr.sin_port = htons(port); if (bind(srv, (sockaddr *)&addr, sizeof(addr)) < 0) { perror("bind"); return 1; } listen(srv, 8); std::cout << "hand_bridge listening on 127.0.0.1:" << port << " (iface " << iface << ")" << std::endl; char buf[1024]; while (true) { int c = accept(srv, nullptr, nullptr); if (c < 0) continue; int n = read(c, buf, sizeof(buf) - 1); if (n > 0) { buf[n] = 0; if (buf[0] == 'F' || buf[0] == 'f') // follow: "F 1 gain dead maxrate force" | "F 0" { std::istringstream iss(buf + 1); int on = 0; double g = 0, d = 0, mr = 0, fo = 0; iss >> on; if (iss >> g && g > 0) fgain.store(g); if (iss >> d && d > 0) fdead.store(d); if (iss >> mr && mr > 0) fmaxrate.store(mr); if (iss >> fo && fo > 0) fforce.store(fo); if (on) fcal.store(true); // re-zero the baseline each time it arms follow.store(on != 0); std::string o = std::string("follow ") + (on ? "on" : "off") + "\n"; (void)!write(c, o.c_str(), o.size()); } else if (buf[0] == 'D' || buf[0] == 'd') // full diagnostics: 8x12 + rate { std::string out; { std::lock_guard lk(mtx); char t[32]; const double *blocks[8] = {state, force, cur, fset, temp, err, sta, lost}; for (int b = 0; b < 8; b++) for (int i = 0; i < 12; i++) { snprintf(t, sizeof(t), "%.3f ", blocks[b][i]); out += t; } snprintf(t, sizeof(t), "%.2f ", rate); out += t; } out += "\n"; (void)!write(c, out.c_str(), out.size()); } else if (buf[0] == 'S' || buf[0] == 's') // angle + force: 24 values { std::string out; { std::lock_guard lk(mtx); char t[32]; for (int i = 0; i < 12; i++) { snprintf(t, sizeof(t), "%.3f ", state[i]); out += t; } for (int i = 0; i < 12; i++) { snprintf(t, sizeof(t), "%.1f ", force[i]); out += t; } } out += "\n"; (void)!write(c, out.c_str(), out.size()); } else if (buf[0] == 'R' || buf[0] == 'r') // read-back request (12 angles, legacy) { std::string out; { std::lock_guard lk(mtx); char t[32]; for (int i = 0; i < 12; i++) { snprintf(t, sizeof(t), "%.3f ", state[i]); out += t; } } out += "\n"; (void)!write(c, out.c_str(), out.size()); } else { std::istringstream iss(buf); std::vector v; float x; while (iss >> x) v.push_back(x); if (v.size() == 12) { for (int i = 0; i < 12; i++) cmd.cmds()[i].q() = v[i]; for (int k = 0; k < 3; k++) { pub->Write(cmd); usleep(8000); } (void)!write(c, "ok\n", 3); } else (void)!write(c, "err\n", 4); } } close(c); } return 0; }