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.
Files changed (94) hide show
  1. roboapi/__init__.py +2 -0
  2. roboapi/calibration/__init__.py +6 -0
  3. roboapi/calibration/profile.py +139 -0
  4. roboapi/calibration/session.py +350 -0
  5. roboapi/cli.py +215 -0
  6. roboapi/config/__init__.py +22 -0
  7. roboapi/config/loader.py +119 -0
  8. roboapi/config/models.py +191 -0
  9. roboapi/embodiment.py +251 -0
  10. roboapi/errors.py +37 -0
  11. roboapi/geometry.py +337 -0
  12. roboapi/hardware/__init__.py +5 -0
  13. roboapi/hardware/base.py +113 -0
  14. roboapi/hardware/camera.py +167 -0
  15. roboapi/hardware/franka/__init__.py +5 -0
  16. roboapi/hardware/franka/arm.py +476 -0
  17. roboapi/hardware/franka/desk.py +555 -0
  18. roboapi/hardware/franka/feedback.py +196 -0
  19. roboapi/hardware/franka/guiding.py +104 -0
  20. roboapi/hardware/franka/ik.py +543 -0
  21. roboapi/hardware/franka/limits.py +110 -0
  22. roboapi/hardware/franka/settings.py +116 -0
  23. roboapi/hardware/robotiq.py +350 -0
  24. roboapi/hardware/ur/__init__.py +16 -0
  25. roboapi/hardware/ur/build.py +92 -0
  26. roboapi/hardware/ur/compliant.py +128 -0
  27. roboapi/hardware/ur/dashboard.py +103 -0
  28. roboapi/hardware/ur/feedback.py +159 -0
  29. roboapi/hardware/ur/ipc.py +206 -0
  30. roboapi/hardware/ur/native/CMakeLists.txt +37 -0
  31. roboapi/hardware/ur/native/dependencies.lock.json +13 -0
  32. roboapi/hardware/ur/native/include/ur_cpp/cartesian_speedl.hpp +170 -0
  33. roboapi/hardware/ur/native/include/ur_cpp/control_loop.hpp +126 -0
  34. roboapi/hardware/ur/native/include/ur_cpp/geom.hpp +72 -0
  35. roboapi/hardware/ur/native/include/ur_cpp/motion_primitives.hpp +350 -0
  36. roboapi/hardware/ur/native/include/ur_cpp/robot_io.hpp +26 -0
  37. roboapi/hardware/ur/native/include/ur_cpp/streaming_pose.hpp +369 -0
  38. roboapi/hardware/ur/native/src/control_loop.cpp +512 -0
  39. roboapi/hardware/ur/native/src/robot_io_urcl.cpp +143 -0
  40. roboapi/hardware/ur/native/tests/cartesian_speedl_unit.cpp +113 -0
  41. roboapi/hardware/ur/native/tests/control_modes_unit.cpp +137 -0
  42. roboapi/hardware/ur/native/tests/streaming_pose_unit.cpp +348 -0
  43. roboapi/hardware/ur/native/tests/velocity_governor_unit.cpp +65 -0
  44. roboapi/hardware/ur/native/worker.cpp +147 -0
  45. roboapi/hardware/ur/position.py +253 -0
  46. roboapi/hardware/ur/settings.py +95 -0
  47. roboapi/images.py +16 -0
  48. roboapi/initialize.py +137 -0
  49. roboapi/mcp.py +497 -0
  50. roboapi/motion.py +125 -0
  51. roboapi/naming.py +7 -0
  52. roboapi/operations.py +45 -0
  53. roboapi/policy/__init__.py +7 -0
  54. roboapi/policy/client.py +277 -0
  55. roboapi/policy/protocol.py +166 -0
  56. roboapi/policy/settings.py +79 -0
  57. roboapi/product-names.json +12 -0
  58. roboapi/records.py +149 -0
  59. roboapi/runtime.py +911 -0
  60. roboapi/semantics.py +6 -0
  61. roboapi/tasks.py +337 -0
  62. roboapi/web/__init__.py +5 -0
  63. roboapi/web/presets.py +44 -0
  64. roboapi/web/server.py +419 -0
  65. roboapi/web/static/assets/Bundler-LICENSE.txt +48 -0
  66. roboapi/web/static/assets/CalibrationScene-BO_spRoL.js +4220 -0
  67. roboapi/web/static/assets/Inter-OFL.txt +93 -0
  68. roboapi/web/static/assets/Inter-Variable-c8O0ljhh.ttf +0 -0
  69. roboapi/web/static/assets/Lucide-LICENSE.txt +43 -0
  70. roboapi/web/static/assets/Meshoptimizer-LICENSE.txt +23 -0
  71. roboapi/web/static/assets/MiSans-Regular-ES4DrbaF.woff2 +0 -0
  72. roboapi/web/static/assets/MiSans-Semibold-B34HIk9C.woff2 +0 -0
  73. roboapi/web/static/assets/PokeBrand-NOTICE.txt +20 -0
  74. roboapi/web/static/assets/React-LICENSE.txt +23 -0
  75. roboapi/web/static/assets/Three-LICENSE.txt +23 -0
  76. roboapi/web/static/assets/home-reference-CTpf8-oV.png +0 -0
  77. roboapi/web/static/assets/index-CGLhpUm0.js +28 -0
  78. roboapi/web/static/assets/index-QXqV-fDC.css +1 -0
  79. roboapi/web/static/assets/robotiq-ace-pro-2-EJuoOXTy.glb +0 -0
  80. roboapi/web/static/index.html +15 -0
  81. roboapi-0.4.2.dist-info/METADATA +205 -0
  82. roboapi-0.4.2.dist-info/RECORD +94 -0
  83. roboapi-0.4.2.dist-info/WHEEL +5 -0
  84. roboapi-0.4.2.dist-info/entry_points.txt +2 -0
  85. roboapi-0.4.2.dist-info/licenses/LICENSE +202 -0
  86. roboapi-0.4.2.dist-info/licenses/NOTICE +16 -0
  87. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Bundler-LICENSE.txt +48 -0
  88. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Inter-OFL.txt +93 -0
  89. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Lucide-LICENSE.txt +43 -0
  90. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Meshoptimizer-LICENSE.txt +23 -0
  91. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/PokeBrand-NOTICE.txt +20 -0
  92. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/React-LICENSE.txt +23 -0
  93. roboapi-0.4.2.dist-info/licenses/frontend/public/assets/Three-LICENSE.txt +23 -0
  94. 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