roboapi 0.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- roboapi/__init__.py +2 -0
- roboapi/calibration/__init__.py +6 -0
- roboapi/calibration/profile.py +139 -0
- roboapi/calibration/session.py +350 -0
- roboapi/cli.py +215 -0
- roboapi/config/__init__.py +22 -0
- roboapi/config/loader.py +119 -0
- roboapi/config/models.py +191 -0
- roboapi/embodiment.py +251 -0
- roboapi/errors.py +37 -0
- roboapi/geometry.py +337 -0
- roboapi/hardware/__init__.py +5 -0
- roboapi/hardware/base.py +113 -0
- roboapi/hardware/camera.py +167 -0
- roboapi/hardware/franka/__init__.py +5 -0
- roboapi/hardware/franka/arm.py +476 -0
- roboapi/hardware/franka/desk.py +555 -0
- roboapi/hardware/franka/feedback.py +196 -0
- roboapi/hardware/franka/guiding.py +104 -0
- roboapi/hardware/franka/ik.py +543 -0
- roboapi/hardware/franka/limits.py +110 -0
- roboapi/hardware/franka/settings.py +116 -0
- roboapi/hardware/robotiq.py +350 -0
- roboapi/hardware/ur/__init__.py +16 -0
- roboapi/hardware/ur/build.py +92 -0
- roboapi/hardware/ur/compliant.py +128 -0
- roboapi/hardware/ur/dashboard.py +103 -0
- roboapi/hardware/ur/feedback.py +159 -0
- roboapi/hardware/ur/ipc.py +206 -0
- roboapi/hardware/ur/native/CMakeLists.txt +37 -0
- roboapi/hardware/ur/native/dependencies.lock.json +13 -0
- roboapi/hardware/ur/native/include/ur_cpp/cartesian_speedl.hpp +170 -0
- roboapi/hardware/ur/native/include/ur_cpp/control_loop.hpp +126 -0
- roboapi/hardware/ur/native/include/ur_cpp/geom.hpp +72 -0
- roboapi/hardware/ur/native/include/ur_cpp/motion_primitives.hpp +350 -0
- roboapi/hardware/ur/native/include/ur_cpp/robot_io.hpp +26 -0
- roboapi/hardware/ur/native/include/ur_cpp/streaming_pose.hpp +369 -0
- roboapi/hardware/ur/native/src/control_loop.cpp +512 -0
- roboapi/hardware/ur/native/src/robot_io_urcl.cpp +143 -0
- roboapi/hardware/ur/native/tests/cartesian_speedl_unit.cpp +113 -0
- roboapi/hardware/ur/native/tests/control_modes_unit.cpp +137 -0
- roboapi/hardware/ur/native/tests/streaming_pose_unit.cpp +348 -0
- roboapi/hardware/ur/native/tests/velocity_governor_unit.cpp +65 -0
- roboapi/hardware/ur/native/worker.cpp +147 -0
- roboapi/hardware/ur/position.py +253 -0
- roboapi/hardware/ur/settings.py +95 -0
- roboapi/images.py +16 -0
- roboapi/initialize.py +137 -0
- roboapi/mcp.py +497 -0
- roboapi/motion.py +125 -0
- roboapi/naming.py +7 -0
- roboapi/operations.py +45 -0
- roboapi/policy/__init__.py +7 -0
- roboapi/policy/client.py +277 -0
- roboapi/policy/protocol.py +166 -0
- roboapi/policy/settings.py +79 -0
- roboapi/product-names.json +12 -0
- roboapi/records.py +149 -0
- roboapi/runtime.py +911 -0
- roboapi/semantics.py +6 -0
- roboapi/tasks.py +337 -0
- roboapi/web/__init__.py +5 -0
- roboapi/web/presets.py +44 -0
- roboapi/web/server.py +419 -0
- roboapi/web/static/assets/Bundler-LICENSE.txt +48 -0
- roboapi/web/static/assets/CalibrationScene-BO_spRoL.js +4220 -0
- roboapi/web/static/assets/Inter-OFL.txt +93 -0
- roboapi/web/static/assets/Inter-Variable-c8O0ljhh.ttf +0 -0
- roboapi/web/static/assets/Lucide-LICENSE.txt +43 -0
- roboapi/web/static/assets/Meshoptimizer-LICENSE.txt +23 -0
- roboapi/web/static/assets/MiSans-Regular-ES4DrbaF.woff2 +0 -0
- roboapi/web/static/assets/MiSans-Semibold-B34HIk9C.woff2 +0 -0
- roboapi/web/static/assets/PokeBrand-NOTICE.txt +20 -0
- roboapi/web/static/assets/React-LICENSE.txt +23 -0
- roboapi/web/static/assets/Three-LICENSE.txt +23 -0
- roboapi/web/static/assets/home-reference-CTpf8-oV.png +0 -0
- roboapi/web/static/assets/index-CGLhpUm0.js +28 -0
- roboapi/web/static/assets/index-QXqV-fDC.css +1 -0
- roboapi/web/static/assets/robotiq-ace-pro-2-EJuoOXTy.glb +0 -0
- roboapi/web/static/index.html +15 -0
- roboapi-0.4.2.dist-info/METADATA +205 -0
- roboapi-0.4.2.dist-info/RECORD +94 -0
- roboapi-0.4.2.dist-info/WHEEL +5 -0
- roboapi-0.4.2.dist-info/entry_points.txt +2 -0
- roboapi-0.4.2.dist-info/licenses/LICENSE +202 -0
- roboapi-0.4.2.dist-info/licenses/NOTICE +16 -0
- roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Bundler-LICENSE.txt +48 -0
- roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Inter-OFL.txt +93 -0
- roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Lucide-LICENSE.txt +43 -0
- roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Meshoptimizer-LICENSE.txt +23 -0
- roboapi-0.4.2.dist-info/licenses/frontend/public/assets/PokeBrand-NOTICE.txt +20 -0
- roboapi-0.4.2.dist-info/licenses/frontend/public/assets/React-LICENSE.txt +23 -0
- roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Three-LICENSE.txt +23 -0
- roboapi-0.4.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// IPC adapter for the UR control runtime.
|
|
2
|
+
#include "ur_cpp/control_loop.hpp"
|
|
3
|
+
#include "ur_cpp/robot_io.hpp"
|
|
4
|
+
#include <algorithm>
|
|
5
|
+
#include <cerrno>
|
|
6
|
+
#include <csignal>
|
|
7
|
+
#include <iomanip>
|
|
8
|
+
#include <iostream>
|
|
9
|
+
#include <map>
|
|
10
|
+
#include <sstream>
|
|
11
|
+
#include <stdexcept>
|
|
12
|
+
#include <sys/socket.h>
|
|
13
|
+
#include <unistd.h>
|
|
14
|
+
#include <vector>
|
|
15
|
+
|
|
16
|
+
std::unique_ptr<RobotIO> openLiveRobotIo(const Args &, std::atomic<bool> &, std::string *);
|
|
17
|
+
std::unique_ptr<RobotIO> openRobotIo(const Args &a, std::atomic<bool> &s, std::string *e) {
|
|
18
|
+
return openLiveRobotIo(a, s, e);
|
|
19
|
+
}
|
|
20
|
+
static volatile std::sig_atomic_t exiting = 0;
|
|
21
|
+
static void onSignal(int) { exiting = 1; }
|
|
22
|
+
|
|
23
|
+
static bool transfer(int fd, void *data, size_t size, bool sending) {
|
|
24
|
+
auto *p = static_cast<char *>(data);
|
|
25
|
+
while (size) {
|
|
26
|
+
const auto n = sending ? ::send(fd, p, size, MSG_NOSIGNAL) : ::recv(fd, p, size, 0);
|
|
27
|
+
if (n < 0 && errno == EINTR && !exiting) continue;
|
|
28
|
+
if (n <= 0) return false;
|
|
29
|
+
p += n;
|
|
30
|
+
size -= static_cast<size_t>(n);
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
static void reply(int fd, const Snapshot &s) {
|
|
35
|
+
std::array<double, 43> values{};
|
|
36
|
+
size_t i = 0;
|
|
37
|
+
for (const auto &v : {s.q_, s.qd_, s.tcp_, s.tcp_speed_, s.wrench_})
|
|
38
|
+
for (double x : v) values[i++] = x;
|
|
39
|
+
for (double x : {double(s.mode_), double(s.fault_), double(s.safety_mode_), double(s.ticks_),
|
|
40
|
+
s.dt_mean_ms_, s.dt_std_ms_, s.dt_p99_ms_, s.dt_max_ms_, s.rtde_hz_,
|
|
41
|
+
s.host_hz_, double(s.program_running_), double(s.loop_alive_), double(s.loop_ok_)})
|
|
42
|
+
values[i++] = x;
|
|
43
|
+
auto length = static_cast<uint32_t>(s.last_error_.size());
|
|
44
|
+
if (!transfer(fd, values.data(), sizeof(values), true) ||
|
|
45
|
+
!transfer(fd, &length, sizeof(length), true) ||
|
|
46
|
+
(length && !transfer(fd, const_cast<char *>(s.last_error_.data()), length, true)))
|
|
47
|
+
throw std::runtime_error("UR IPC closed");
|
|
48
|
+
}
|
|
49
|
+
static std::vector<double> numbers(std::string text, size_t count) {
|
|
50
|
+
std::replace(text.begin(), text.end(), ',', ' ');
|
|
51
|
+
std::istringstream stream(text);
|
|
52
|
+
std::vector<double> values;
|
|
53
|
+
double x;
|
|
54
|
+
while (stream >> x) values.push_back(x);
|
|
55
|
+
if (!stream.eof() || values.size() != count)
|
|
56
|
+
throw std::invalid_argument("incorrect parameter vector size");
|
|
57
|
+
return values;
|
|
58
|
+
}
|
|
59
|
+
static void dumpArgs(const Args &a) {
|
|
60
|
+
std::cout << std::setprecision(17) << "{\"fastloop_freq\":" << a.freq_
|
|
61
|
+
<< ",\"fastloop_rt_prio\":" << a.rt_prio_ << ",\"fastloop_pos_loop_kp\":" << a.pos_kp_
|
|
62
|
+
<< ",\"fastloop_damping_ratio\":" << a.tun_.zeta_ << ",\"fastloop_filter_tau\":" << a.tun_.filter_tau_
|
|
63
|
+
<< ",\"fastloop_wrench_notch\":" << a.tun_.notch_freq_ << ",\"fastloop_max_dt\":" << a.tun_.max_dt_
|
|
64
|
+
<< ",\"fastloop_max_vel\":" << a.tun_.max_vel_ << ",\"fastloop_relaxation_hz\":" << a.tun_.relaxation_hz_;
|
|
65
|
+
const auto pair = [](const char *key, double x, double y) {
|
|
66
|
+
std::cout << ",\"" << key << "\":[" << x << ',' << y << ']';
|
|
67
|
+
};
|
|
68
|
+
pair("fastloop_mass", a.tun_.M_[0], a.tun_.M_[3]);
|
|
69
|
+
pair("fastloop_stiffness", a.tun_.K_[0], a.tun_.K_[3]);
|
|
70
|
+
pair("fastloop_hf_mass_ratio", a.tun_.hf_mass_ratio_[0], a.tun_.hf_mass_ratio_[3]);
|
|
71
|
+
pair("fastloop_wrench_deadband", a.tun_.deadband_force_, a.tun_.deadband_torque_);
|
|
72
|
+
pair("fastloop_cmd_vel_limit", a.cmd_vel_trans_, a.cmd_vel_rot_);
|
|
73
|
+
pair("fastloop_cmd_acc_limit", a.cmd_acc_trans_, a.cmd_acc_rot_);
|
|
74
|
+
pair("fastloop_speedl_acc_limit", a.speedl_acc_trans_, a.speedl_acc_rot_);
|
|
75
|
+
pair("fastloop_speedl_jerk_limit", a.speedl_jerk_trans_, a.speedl_jerk_rot_);
|
|
76
|
+
std::cout << "}\n";
|
|
77
|
+
}
|
|
78
|
+
int main(int argc, char **argv) {
|
|
79
|
+
std::signal(SIGINT, onSignal);
|
|
80
|
+
std::signal(SIGTERM, onSignal);
|
|
81
|
+
Args a;
|
|
82
|
+
int fd = -1;
|
|
83
|
+
bool checking = false, zero_on_start = true;
|
|
84
|
+
const std::map<std::string, double *> scalars{
|
|
85
|
+
{"fastloop_freq", &a.freq_},
|
|
86
|
+
{"fastloop_pos_loop_kp", &a.pos_kp_}, {"fastloop_damping_ratio", &a.tun_.zeta_},
|
|
87
|
+
{"fastloop_filter_tau", &a.tun_.filter_tau_}, {"fastloop_wrench_notch", &a.tun_.notch_freq_},
|
|
88
|
+
{"fastloop_max_dt", &a.tun_.max_dt_}, {"fastloop_max_vel", &a.tun_.max_vel_}, {"fastloop_relaxation_hz", &a.tun_.relaxation_hz_}};
|
|
89
|
+
const std::map<std::string, std::pair<double *, double *>> pairs{
|
|
90
|
+
{"fastloop_cmd_vel_limit", {&a.cmd_vel_trans_, &a.cmd_vel_rot_}},
|
|
91
|
+
{"fastloop_cmd_acc_limit", {&a.cmd_acc_trans_, &a.cmd_acc_rot_}},
|
|
92
|
+
{"fastloop_speedl_acc_limit", {&a.speedl_acc_trans_, &a.speedl_acc_rot_}},
|
|
93
|
+
{"fastloop_speedl_jerk_limit", {&a.speedl_jerk_trans_, &a.speedl_jerk_rot_}},
|
|
94
|
+
{"fastloop_wrench_deadband", {&a.tun_.deadband_force_, &a.tun_.deadband_torque_}}};
|
|
95
|
+
try {
|
|
96
|
+
for (int i = 1; i < argc; ++i) {
|
|
97
|
+
const std::string key = argv[i];
|
|
98
|
+
if (key == "--check-config") { checking = true; continue; }
|
|
99
|
+
if (i + 1 == argc || key.rfind("--", 0) != 0) throw std::invalid_argument("expected --parameter value");
|
|
100
|
+
const std::string name = key.substr(2), value = argv[++i];
|
|
101
|
+
if (name == "ip") a.robot_ip_ = value;
|
|
102
|
+
else if (name == "script") a.script_file_ = value;
|
|
103
|
+
else if (name == "ipc-fd") fd = std::stoi(value);
|
|
104
|
+
else if (name == "zero_ft_on_start") zero_on_start = numbers(value, 1)[0] != 0;
|
|
105
|
+
else if (name == "fastloop_rt_prio") a.rt_prio_ = static_cast<int>(numbers(value, 1)[0]);
|
|
106
|
+
else if (scalars.count(name)) *scalars.at(name) = numbers(value, 1)[0];
|
|
107
|
+
else if (pairs.count(name)) {
|
|
108
|
+
const auto v = numbers(value, 2);
|
|
109
|
+
*pairs.at(name).first = v[0]; *pairs.at(name).second = v[1];
|
|
110
|
+
} else if (name == "fastloop_mass" || name == "fastloop_stiffness" || name == "fastloop_hf_mass_ratio") {
|
|
111
|
+
const auto v = numbers(value, 2);
|
|
112
|
+
double *out = name == "fastloop_mass" ? a.tun_.M_ : name == "fastloop_stiffness" ? a.tun_.K_ : a.tun_.hf_mass_ratio_;
|
|
113
|
+
for (int k = 0; k < 6; ++k) out[k] = v[k < 3 ? 0 : 1];
|
|
114
|
+
} else throw std::invalid_argument("unknown UR driver parameter: " + name);
|
|
115
|
+
}
|
|
116
|
+
std::string error;
|
|
117
|
+
if (!validateArgs(a, error)) throw std::invalid_argument(error);
|
|
118
|
+
if (checking) { dumpArgs(a); return 0; }
|
|
119
|
+
if (fd < 0 || a.robot_ip_.empty() || a.script_file_.empty())
|
|
120
|
+
throw std::invalid_argument("IPC fd, robot address and URScript required");
|
|
121
|
+
ControlLoop loop(a);
|
|
122
|
+
loop.start();
|
|
123
|
+
const bool ready = loop.waitReady(12.0);
|
|
124
|
+
if (ready && zero_on_start) loop.zeroFt();
|
|
125
|
+
auto initial = loop.snapshot();
|
|
126
|
+
if (!ready && initial.last_error_.empty()) initial.last_error_ = "fastloop_start_failed";
|
|
127
|
+
reply(fd, initial);
|
|
128
|
+
while (ready && !exiting) {
|
|
129
|
+
uint8_t op;
|
|
130
|
+
if (!transfer(fd, &op, sizeof(op), false)) break;
|
|
131
|
+
vec6 target{};
|
|
132
|
+
if ((op == 'P' || op == 'C') && !transfer(fd, target.data(), sizeof(target), false)) break;
|
|
133
|
+
if (op == 'P' || op == 'C') loop.setPoseRef(target, op == 'P');
|
|
134
|
+
else if (op == 'H') loop.stopMotion();
|
|
135
|
+
else if (op == 'Z') loop.zeroFt();
|
|
136
|
+
else if (op == 'Q') { loop.requestStop(); reply(fd, loop.snapshot()); break; }
|
|
137
|
+
else if (op != 'S') throw std::invalid_argument("unknown IPC operation");
|
|
138
|
+
reply(fd, loop.snapshot());
|
|
139
|
+
}
|
|
140
|
+
loop.requestStop();
|
|
141
|
+
loop.join();
|
|
142
|
+
} catch (const std::exception &e) {
|
|
143
|
+
std::cerr << "[adapter] " << e.what() << '\n';
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Position-only UR CB3 control using ur_rtde."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from ... import geometry as geo
|
|
12
|
+
from .dashboard import Dashboard
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
NUM_JOINTS = 6
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_SAFETY_MODE_NAMES = {
|
|
20
|
+
1: "normal", 2: "reduced", 3: "protective_stop", 4: "recovery", 5: "safeguard_stop",
|
|
21
|
+
6: "system_emergency_stop", 7: "robot_emergency_stop", 8: "violation", 9: "fault",
|
|
22
|
+
10: "validate_joint_id", 11: "undefined", 12: "automatic_mode_safeguard_stop",
|
|
23
|
+
13: "system_three_position_enabling_stop",
|
|
24
|
+
}
|
|
25
|
+
_EXTERNAL_RESET_SAFETY_MODES = frozenset({4, 5, 6, 7, 12, 13})
|
|
26
|
+
_FAULTED_SAFETY_MODES = frozenset({8, 9, 10, 11})
|
|
27
|
+
_ROBOT_MODE_NAMES = {
|
|
28
|
+
-1: "no_controller", 0: "disconnected", 1: "confirm_safety", 2: "booting", 3: "power_off",
|
|
29
|
+
4: "power_on", 5: "idle", 6: "backdrive", 7: "running", 8: "updating_firmware",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PositionArm:
|
|
34
|
+
"""CB3 position control through a paced RTDE servoL stream."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, cfg) -> None:
|
|
37
|
+
self.cfg = cfg
|
|
38
|
+
self._c = None
|
|
39
|
+
self._r = None
|
|
40
|
+
self._error: str | None = None
|
|
41
|
+
# One owner for vendor calls and targets; RTDEControlInterface is not thread-safe.
|
|
42
|
+
self._lock = threading.RLock()
|
|
43
|
+
self._target: np.ndarray | None = None
|
|
44
|
+
self._stop = threading.Event()
|
|
45
|
+
self._paused = threading.Event() # held during controller recovery
|
|
46
|
+
self._thread: threading.Thread | None = None
|
|
47
|
+
self._period = 1.0 / float(cfg.servo_hz)
|
|
48
|
+
self._dashboard = Dashboard(cfg.ip)
|
|
49
|
+
self._version: tuple[int, int] | None = None
|
|
50
|
+
|
|
51
|
+
# -- connection --------------------------------------------------------
|
|
52
|
+
@property
|
|
53
|
+
def live(self) -> bool:
|
|
54
|
+
with self._lock:
|
|
55
|
+
if self._c is None or self._r is None or self._error is not None:
|
|
56
|
+
return False
|
|
57
|
+
try:
|
|
58
|
+
if not self._c.isConnected() or not self._r.isConnected():
|
|
59
|
+
self._error = "rtde_disconnected"
|
|
60
|
+
elif not self._c.isProgramRunning():
|
|
61
|
+
self._error = "control_script_stopped"
|
|
62
|
+
except Exception:
|
|
63
|
+
self._error = "rtde_disconnected"
|
|
64
|
+
return self._error is None
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def last_error(self) -> str | None:
|
|
68
|
+
return self._error
|
|
69
|
+
|
|
70
|
+
def _connect(self) -> None:
|
|
71
|
+
import rtde_control
|
|
72
|
+
import rtde_receive
|
|
73
|
+
|
|
74
|
+
self._version = self._dashboard.version()
|
|
75
|
+
if self._version[0] != 3:
|
|
76
|
+
raise ValueError("CB3 position control requires a PolyScope 3 controller")
|
|
77
|
+
self._c = rtde_control.RTDEControlInterface(self.cfg.ip)
|
|
78
|
+
# Receive follows the configured CB3 servo rate.
|
|
79
|
+
self._r = rtde_receive.RTDEReceiveInterface(self.cfg.ip, float(self.cfg.servo_hz))
|
|
80
|
+
self._error = None
|
|
81
|
+
if self._c.setWatchdog(self.cfg.watchdog_hz) is False:
|
|
82
|
+
raise RuntimeError("RTDE watchdog setup rejected")
|
|
83
|
+
|
|
84
|
+
def connect(self) -> None:
|
|
85
|
+
try:
|
|
86
|
+
with self._lock:
|
|
87
|
+
self._connect()
|
|
88
|
+
except Exception:
|
|
89
|
+
self._close()
|
|
90
|
+
raise
|
|
91
|
+
self._stop.clear()
|
|
92
|
+
self._thread = threading.Thread(target=self._servo_loop, name="ur-servo", daemon=True)
|
|
93
|
+
self._thread.start()
|
|
94
|
+
|
|
95
|
+
def disconnect(self) -> None:
|
|
96
|
+
self._stop.set()
|
|
97
|
+
if self._thread is not None:
|
|
98
|
+
self._thread.join(timeout=2.0)
|
|
99
|
+
self._close()
|
|
100
|
+
|
|
101
|
+
def _close(self) -> None:
|
|
102
|
+
with self._lock:
|
|
103
|
+
if self._c is not None:
|
|
104
|
+
try:
|
|
105
|
+
self.hold()
|
|
106
|
+
except Exception:
|
|
107
|
+
logger.warning("UR stop could not be confirmed during disconnect", exc_info=True)
|
|
108
|
+
try:
|
|
109
|
+
if self._c.isConnected() and self._c.isProgramRunning():
|
|
110
|
+
if self._c.stopScript() is False:
|
|
111
|
+
logger.warning("UR control script stop rejected")
|
|
112
|
+
except Exception:
|
|
113
|
+
logger.warning("UR control script stop could not be confirmed", exc_info=True)
|
|
114
|
+
for iface in (self._c, self._r):
|
|
115
|
+
try:
|
|
116
|
+
if iface is not None:
|
|
117
|
+
iface.disconnect()
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
self._c = self._r = None
|
|
121
|
+
self._target = None
|
|
122
|
+
|
|
123
|
+
# -- state -------------------------------------------------------------
|
|
124
|
+
def get_joints(self) -> np.ndarray:
|
|
125
|
+
return np.asarray(self._r.getActualQ(), dtype=np.float64)
|
|
126
|
+
|
|
127
|
+
def get_eef_pose(self) -> np.ndarray:
|
|
128
|
+
return geo.rotvec6_to_pose7(np.asarray(self._r.getActualTCPPose(), dtype=np.float64))
|
|
129
|
+
|
|
130
|
+
def get_tcp_offset(self) -> np.ndarray:
|
|
131
|
+
return np.asarray(self._c.getTCPOffset(), dtype=np.float64)
|
|
132
|
+
|
|
133
|
+
def get_tcp_speed(self) -> np.ndarray:
|
|
134
|
+
return np.asarray(self._r.getActualTCPSpeed(), dtype=np.float64)
|
|
135
|
+
|
|
136
|
+
# -- commands ----------------------------------------------------------
|
|
137
|
+
def servo_pose(self, pose7: np.ndarray, twist: np.ndarray | None = None, *, position_only=False) -> None:
|
|
138
|
+
del twist, position_only # CB3 always uses position control.
|
|
139
|
+
p = np.asarray(pose7, dtype=np.float64)
|
|
140
|
+
if p.shape != (7,):
|
|
141
|
+
raise ValueError(f"servo_pose expects pose7d, got shape {p.shape}")
|
|
142
|
+
if not self.live:
|
|
143
|
+
raise RuntimeError(f"not_alive: {self._error}. Call recover().")
|
|
144
|
+
with self._lock:
|
|
145
|
+
if self._paused.is_set():
|
|
146
|
+
return # Recovery owns the connection; do not queue a stale pose.
|
|
147
|
+
self._target = p.copy()
|
|
148
|
+
|
|
149
|
+
def hold(self) -> None:
|
|
150
|
+
"""Stop the active servo and clear queued targets."""
|
|
151
|
+
with self._lock:
|
|
152
|
+
self._target = None
|
|
153
|
+
if self._c is None or not self._c.isConnected() or not self._c.isProgramRunning():
|
|
154
|
+
return
|
|
155
|
+
if self._c.servoStop() is False:
|
|
156
|
+
self._error = "servo_stop_rejected"
|
|
157
|
+
raise RuntimeError(self._error)
|
|
158
|
+
|
|
159
|
+
def _servo_loop(self) -> None:
|
|
160
|
+
"""Latest Cartesian target to servoL, paced by absolute deadlines."""
|
|
161
|
+
next_at = time.monotonic()
|
|
162
|
+
while not self._stop.is_set():
|
|
163
|
+
next_at += self._period
|
|
164
|
+
time.sleep(max(0.0, next_at - time.monotonic()))
|
|
165
|
+
with self._lock:
|
|
166
|
+
if not self.live:
|
|
167
|
+
continue
|
|
168
|
+
try:
|
|
169
|
+
if self._paused.is_set() or self._target is None:
|
|
170
|
+
if self._c.kickWatchdog() is False:
|
|
171
|
+
raise RuntimeError("watchdog_rejected")
|
|
172
|
+
continue
|
|
173
|
+
target = self._target.copy()
|
|
174
|
+
accepted = self._c.servoL(
|
|
175
|
+
geo.pose7_to_rotvec6(target).tolist(), 1.0, 1.4, self._period,
|
|
176
|
+
self.cfg.servo_lookahead_time, self.cfg.servo_gain,
|
|
177
|
+
)
|
|
178
|
+
if accepted is False:
|
|
179
|
+
raise RuntimeError(self._rejection_reason())
|
|
180
|
+
except Exception as exc:
|
|
181
|
+
self._error = f"servo_failed: {exc}"
|
|
182
|
+
try:
|
|
183
|
+
self.hold()
|
|
184
|
+
except Exception:
|
|
185
|
+
logger.warning("UR fault stop could not be confirmed", exc_info=True)
|
|
186
|
+
|
|
187
|
+
def _rejection_reason(self) -> str:
|
|
188
|
+
"""Turn a rejected command into a name a person (or an LLM) can act on."""
|
|
189
|
+
try:
|
|
190
|
+
if not self._c.isConnected() or not self._r.isConnected():
|
|
191
|
+
return "rtde_disconnected"
|
|
192
|
+
safety = int(self._r.getSafetyMode())
|
|
193
|
+
name = _SAFETY_MODE_NAMES.get(safety, f"mode_{safety}")
|
|
194
|
+
if safety == 3:
|
|
195
|
+
return "protective_stop"
|
|
196
|
+
if safety in _EXTERNAL_RESET_SAFETY_MODES:
|
|
197
|
+
return f"external_reset_required:{name}"
|
|
198
|
+
if safety in _FAULTED_SAFETY_MODES:
|
|
199
|
+
return f"safety_fault:{name}"
|
|
200
|
+
robot_mode = int(self._r.getRobotMode())
|
|
201
|
+
if robot_mode != 7:
|
|
202
|
+
return f"robot_not_running:{_ROBOT_MODE_NAMES.get(robot_mode, robot_mode)}"
|
|
203
|
+
if int(self._r.getRobotStatus()) & 0b10 == 0:
|
|
204
|
+
return "control_script_stopped"
|
|
205
|
+
except Exception:
|
|
206
|
+
return "rtde_disconnected"
|
|
207
|
+
return "servo_rejected"
|
|
208
|
+
|
|
209
|
+
# -- recovery ----------------------------------------------------------
|
|
210
|
+
def recover(self, should_stop=None) -> None:
|
|
211
|
+
"""Recover the controller and recreate its RTDE control connection."""
|
|
212
|
+
self._paused.set()
|
|
213
|
+
try:
|
|
214
|
+
self.hold()
|
|
215
|
+
deadline = time.monotonic() + 30.0
|
|
216
|
+
detail = "dashboard_error"
|
|
217
|
+
while time.monotonic() < deadline:
|
|
218
|
+
if should_stop is not None and should_stop():
|
|
219
|
+
raise RuntimeError("UR recovery cancelled")
|
|
220
|
+
ready, detail = self._dashboard.step(should_stop=should_stop)
|
|
221
|
+
if detail == "motion_interrupted":
|
|
222
|
+
raise RuntimeError("UR recovery cancelled")
|
|
223
|
+
if ready:
|
|
224
|
+
break
|
|
225
|
+
if detail.startswith(("external_reset_required", "remote_control_required",
|
|
226
|
+
"confirm_safety_required", "unsupported_")):
|
|
227
|
+
self._error = detail
|
|
228
|
+
raise RuntimeError(
|
|
229
|
+
f"{detail}: a person has to clear this at the robot -- roboapi cannot."
|
|
230
|
+
)
|
|
231
|
+
time.sleep(2.0)
|
|
232
|
+
else:
|
|
233
|
+
self._error = detail
|
|
234
|
+
raise RuntimeError(f"recover timed out: {detail}")
|
|
235
|
+
|
|
236
|
+
# A Dashboard transition invalidates RTDE objects held from before the fault:
|
|
237
|
+
# both sides must be recreated, not reused.
|
|
238
|
+
with self._lock:
|
|
239
|
+
self._close()
|
|
240
|
+
self._connect()
|
|
241
|
+
if not self._c.isProgramRunning() and self._c.reuploadScript() is False:
|
|
242
|
+
self._error = "control_script_reupload_rejected"
|
|
243
|
+
raise RuntimeError(self._error)
|
|
244
|
+
if should_stop is not None and should_stop():
|
|
245
|
+
self.hold()
|
|
246
|
+
raise RuntimeError("UR recovery cancelled")
|
|
247
|
+
q = self.get_joints()
|
|
248
|
+
if q.shape != (NUM_JOINTS,) or not np.all(np.isfinite(q)):
|
|
249
|
+
self._error = "invalid_feedback"
|
|
250
|
+
raise RuntimeError(self._error)
|
|
251
|
+
self._error = None
|
|
252
|
+
finally:
|
|
253
|
+
self._paused.clear()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""UR controller generations and their standard control settings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
import math
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from ...config.models import ArmConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _positive(name, value):
|
|
14
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or value <= 0:
|
|
15
|
+
raise ValueError(f"arm.{name} must be positive and finite")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class URArmConfig(ArmConfig):
|
|
20
|
+
type: str = "ur"
|
|
21
|
+
ip: str = ""
|
|
22
|
+
|
|
23
|
+
def validate(self) -> None:
|
|
24
|
+
if not self.ip:
|
|
25
|
+
raise ValueError("arm.type=ur requires arm.ip")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class CB3ArmConfig(URArmConfig):
|
|
30
|
+
series: str = "cb3"
|
|
31
|
+
servo_hz: float = 100.0
|
|
32
|
+
servo_gain: float = 300.0
|
|
33
|
+
watchdog_hz: float = 2.0
|
|
34
|
+
servo_lookahead_time: float = 0.1
|
|
35
|
+
|
|
36
|
+
def validate(self) -> None:
|
|
37
|
+
super().validate()
|
|
38
|
+
if self.series != "cb3":
|
|
39
|
+
raise ValueError("CB3 configuration requires arm.series=cb3")
|
|
40
|
+
for name in ("servo_hz", "servo_gain", "servo_lookahead_time", "watchdog_hz"):
|
|
41
|
+
_positive(name, getattr(self, name))
|
|
42
|
+
if self.watchdog_hz >= self.servo_hz:
|
|
43
|
+
raise ValueError("arm.watchdog_hz must be below arm.servo_hz")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class ESeriesArmConfig(URArmConfig):
|
|
48
|
+
series: str = "e"
|
|
49
|
+
rate_hz: float = 125.0
|
|
50
|
+
zero_ft_on_start: bool = True
|
|
51
|
+
fastloop_freq: float = 500.0
|
|
52
|
+
fastloop_rt_prio: int = 0
|
|
53
|
+
fastloop_mass: list[float] = field(default_factory=lambda: [6.0, 0.075])
|
|
54
|
+
fastloop_stiffness: list[float] = field(default_factory=lambda: [400.0, 11.25])
|
|
55
|
+
fastloop_damping_ratio: float = 3.5
|
|
56
|
+
fastloop_hf_mass_ratio: list[float] = field(default_factory=lambda: [1.5, 2.0])
|
|
57
|
+
fastloop_relaxation_hz: float = 5.0
|
|
58
|
+
fastloop_filter_tau: float = 0.005
|
|
59
|
+
fastloop_wrench_deadband: list[float] = field(default_factory=lambda: [2.5, 0.075])
|
|
60
|
+
fastloop_wrench_notch: float = 0.0
|
|
61
|
+
fastloop_max_dt: float = 0.015
|
|
62
|
+
fastloop_max_vel: float = 0.35
|
|
63
|
+
fastloop_cmd_vel_limit: list[float] = field(default_factory=lambda: [1.0, 2.5])
|
|
64
|
+
fastloop_cmd_acc_limit: list[float] = field(default_factory=lambda: [3.0, 8.0])
|
|
65
|
+
fastloop_pos_loop_kp: float = 15.0
|
|
66
|
+
fastloop_speedl_acc_limit: list[float] = field(default_factory=lambda: [25.0, 30.0])
|
|
67
|
+
fastloop_speedl_jerk_limit: list[float] = field(default_factory=lambda: [2000.0, 2000.0])
|
|
68
|
+
fastloop_binary: str = field(default_factory=lambda: str(Path(sys.prefix) / "bin/roboapi-ur-loop"))
|
|
69
|
+
fastloop_script: str = field(default_factory=lambda: str(Path(sys.prefix) / "share/roboapi-ur-loop/external_control.urscript"))
|
|
70
|
+
fastloop_log_dir: str = "./logs/ur-native"
|
|
71
|
+
native_rpc_timeout_s: float = 0.5
|
|
72
|
+
native_state_stale_after_s: float = 0.25
|
|
73
|
+
|
|
74
|
+
def validate(self) -> None:
|
|
75
|
+
super().validate()
|
|
76
|
+
if self.series != "e":
|
|
77
|
+
raise ValueError("e-Series configuration requires arm.series=e")
|
|
78
|
+
_positive("rate_hz", self.rate_hz)
|
|
79
|
+
if not isinstance(self.zero_ft_on_start, bool):
|
|
80
|
+
raise ValueError("UR boolean parameters must be boolean")
|
|
81
|
+
# Native validateArgs owns the admissible ranges of the control-law tuning.
|
|
82
|
+
for name, count in (("fastloop_mass", 2), ("fastloop_stiffness", 2),
|
|
83
|
+
("fastloop_hf_mass_ratio", 2), ("fastloop_wrench_deadband", 2),
|
|
84
|
+
("fastloop_cmd_vel_limit", 2), ("fastloop_cmd_acc_limit", 2),
|
|
85
|
+
("fastloop_speedl_acc_limit", 2), ("fastloop_speedl_jerk_limit", 2)):
|
|
86
|
+
values = getattr(self, name)
|
|
87
|
+
if len(values) != count or not all(isinstance(x, (int, float)) and math.isfinite(x) for x in values):
|
|
88
|
+
raise ValueError(f"arm.{name} must contain {count} finite values")
|
|
89
|
+
for name in ("native_rpc_timeout_s", "native_state_stale_after_s"):
|
|
90
|
+
_positive(name, getattr(self, name))
|
|
91
|
+
if self.native_state_stale_after_s < 2.0 / self.rate_hz:
|
|
92
|
+
raise ValueError("native feedback freshness must allow at least two poll periods")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
SERIES = {"cb3": CB3ArmConfig, "e": ESeriesArmConfig}
|
roboapi/images.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Camera previews shared by the web and MCP interfaces."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from PIL import Image
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def preview_jpeg(image: np.ndarray, width: int = 640, quality: int = 75) -> bytes:
|
|
10
|
+
"""Encode a small preview, preserving the camera's field of view and aspect ratio."""
|
|
11
|
+
preview = Image.fromarray(image)
|
|
12
|
+
if preview.width > width:
|
|
13
|
+
preview = preview.resize((width, preview.height * width // preview.width), Image.Resampling.BILINEAR)
|
|
14
|
+
buffer = io.BytesIO()
|
|
15
|
+
preview.save(buffer, format="JPEG", quality=quality)
|
|
16
|
+
return buffer.getvalue()
|
roboapi/initialize.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""One-time interactive installation setup; saves configuration without contacting devices."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from getpass import getpass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import shlex
|
|
8
|
+
|
|
9
|
+
from . import config
|
|
10
|
+
from .config import DEFAULT_CONFIG
|
|
11
|
+
from .hardware.franka.settings import FrankaArmConfig
|
|
12
|
+
from .hardware.ur.settings import SERIES
|
|
13
|
+
from .policy.settings import validate_policy_endpoint, write_policy_token
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _ask(label, default=""):
|
|
17
|
+
while True:
|
|
18
|
+
value = input(f"{label}" + (f" [{default}]" if default else "") + ": ").strip()
|
|
19
|
+
if value or default:
|
|
20
|
+
return value or default
|
|
21
|
+
print("Enter a value.")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _choose(label, choices, default="1"):
|
|
25
|
+
print(label)
|
|
26
|
+
for index, choice in enumerate(choices, 1):
|
|
27
|
+
print(f" {index}. {choice}")
|
|
28
|
+
while True:
|
|
29
|
+
value = _ask("Choose", default)
|
|
30
|
+
if value.isdigit() and 1 <= int(value) <= len(choices):
|
|
31
|
+
return int(value) - 1
|
|
32
|
+
print("Choose one of the listed numbers.")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _device(label, patterns, default):
|
|
36
|
+
devices = sorted({str(path) for pattern in patterns for path in Path("/dev").glob(pattern)})
|
|
37
|
+
if devices:
|
|
38
|
+
print(f"{label} devices:")
|
|
39
|
+
for index, device in enumerate(devices, 1):
|
|
40
|
+
print(f" {index}. {device}")
|
|
41
|
+
while True:
|
|
42
|
+
value = _ask(f"{label} number or path", "1")
|
|
43
|
+
if not value.isdigit():
|
|
44
|
+
return value
|
|
45
|
+
if 1 <= int(value) <= len(devices):
|
|
46
|
+
return devices[int(value) - 1]
|
|
47
|
+
print("Choose a listed number or enter a device path.")
|
|
48
|
+
return _ask(f"{label} path", default)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _secret(label, path):
|
|
52
|
+
while True:
|
|
53
|
+
value = getpass(label + (" [Enter to keep saved value]" if path.is_file() else "") + ": ").strip()
|
|
54
|
+
if value:
|
|
55
|
+
return value
|
|
56
|
+
if path.is_file() and path.read_text().strip():
|
|
57
|
+
return None
|
|
58
|
+
print("Enter a value.")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def initialize(path_text=DEFAULT_CONFIG):
|
|
62
|
+
path = Path(path_text).expanduser().resolve()
|
|
63
|
+
if path.exists() and input(f"Replace existing configuration at {path}? [y/N]: ").strip().lower() != "y":
|
|
64
|
+
print("Configuration kept.")
|
|
65
|
+
return 0
|
|
66
|
+
from .naming import PRODUCT_NAME
|
|
67
|
+
print(f"{PRODUCT_NAME} installation setup")
|
|
68
|
+
kind = _choose("Robot", ["UR CB3 (position control)", "UR e-Series (compliant control)",
|
|
69
|
+
"Franka FR3", "Franka Panda (system software 4.2.1–4.x)"])
|
|
70
|
+
cfg = config.Config()
|
|
71
|
+
desk_password = None
|
|
72
|
+
if kind < 2:
|
|
73
|
+
print("Find the robot address in PolyScope's network settings.")
|
|
74
|
+
cfg.arm = SERIES["cb3" if kind == 0 else "e"](ip=_ask("Robot address"))
|
|
75
|
+
connection = _choose("Robotiq connection", ["Through the UR controller (URCap)",
|
|
76
|
+
"Host serial port (USB / RS-485)"], default="")
|
|
77
|
+
cfg.gripper = (config.GripperConfig(type="urcap") if connection == 0
|
|
78
|
+
else config.GripperConfig(type="modbus", rate_hz=30.0))
|
|
79
|
+
else:
|
|
80
|
+
print("Use the Control cabinet's Shop Floor network address shown in Desk settings.")
|
|
81
|
+
cfg.arm = FrankaArmConfig.for_model(
|
|
82
|
+
"fr3" if kind == 2 else "panda", host=_ask("Robot address"),
|
|
83
|
+
)
|
|
84
|
+
cfg.gripper = config.GripperConfig(type="modbus", rate_hz=30.0)
|
|
85
|
+
cfg.camera.type = "v4l2"
|
|
86
|
+
cfg.camera.device = _device("Wrist camera", ["v4l/by-id/*video-index0"], "/dev/video0")
|
|
87
|
+
if cfg.gripper.type == "modbus":
|
|
88
|
+
cfg.gripper.port = _device("Gripper serial", ["serial/by-id/*"], "/dev/ttyUSB0")
|
|
89
|
+
if kind >= 2:
|
|
90
|
+
cfg.arm.desk_user = _ask("Desk user", "franka")
|
|
91
|
+
cfg.arm.desk_password_file = str(path.parent / "desk-password")
|
|
92
|
+
desk_password = _secret("Desk password", Path(cfg.arm.desk_password_file))
|
|
93
|
+
while True:
|
|
94
|
+
url = _ask("Model Base URL")
|
|
95
|
+
model = _ask("Model", "vision-1")
|
|
96
|
+
try:
|
|
97
|
+
url, model = validate_policy_endpoint(url, model)
|
|
98
|
+
break
|
|
99
|
+
except ValueError as exc:
|
|
100
|
+
print(str(exc))
|
|
101
|
+
cfg.policy = config.PolicyConfig(type="openai", url=url, model=model,
|
|
102
|
+
token_file=str(path.parent / "model-api-key"))
|
|
103
|
+
api_key = _secret("Model API key", Path(cfg.policy.token_file))
|
|
104
|
+
print("After each action chunk, wait before capturing the next observation. Actions run at 30 Hz.")
|
|
105
|
+
default_pause = str(cfg.runtime.inter_chunk_pause_s)
|
|
106
|
+
while True:
|
|
107
|
+
try:
|
|
108
|
+
cfg.runtime.inter_chunk_pause_s = float(_ask("Wait after each action chunk (seconds)", default_pause))
|
|
109
|
+
cfg.runtime.validate()
|
|
110
|
+
break
|
|
111
|
+
except ValueError:
|
|
112
|
+
print("Enter a non-negative finite number of seconds.")
|
|
113
|
+
print("Runs have no automatic time limit. Stop through MCP, the browser, or Ctrl+C.")
|
|
114
|
+
cfg.runtime.log_dir = str(path.parent / "logs")
|
|
115
|
+
cfg.calibration.profile_file = str(path.parent / "calibration.json")
|
|
116
|
+
cfg.calibration.tool_id = "robotiq-flange-center-v1"
|
|
117
|
+
cfg.validate()
|
|
118
|
+
|
|
119
|
+
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
120
|
+
secrets = [(cfg.policy.token_file, api_key)]
|
|
121
|
+
if kind >= 2:
|
|
122
|
+
secrets.append((cfg.arm.desk_password_file, desk_password))
|
|
123
|
+
for secret_path, value in secrets:
|
|
124
|
+
if value is not None:
|
|
125
|
+
write_policy_token(secret_path, value)
|
|
126
|
+
else:
|
|
127
|
+
Path(secret_path).chmod(0o600)
|
|
128
|
+
config.save(cfg, path)
|
|
129
|
+
print(f"Saved configuration: {path}")
|
|
130
|
+
if cfg.arm.type == "franka":
|
|
131
|
+
print("Franka setup uses the standard assembly load and a flange-centre TCP with aligned axes.")
|
|
132
|
+
print("FR3 5.6 / Panda 4.x: run 'roboapi export-franka-tool' and apply the preset in Desk before calibration.")
|
|
133
|
+
print(f"Browser: roboapi http -c {shlex.quote(str(path))}")
|
|
134
|
+
print(f"MCP: roboapi serve -c {shlex.quote(str(path))}")
|
|
135
|
+
print("Then open http://127.0.0.1:8080/ and choose Devices > Calibrate to set up coordinates and enable control.")
|
|
136
|
+
print("Later, use Settings on http://127.0.0.1:8080/ to change the model connection or chunk wait.")
|
|
137
|
+
return 0
|