urkit 0.4.4__tar.gz → 0.4.6__tar.gz
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.
- {urkit-0.4.4 → urkit-0.4.6}/PKG-INFO +1 -1
- {urkit-0.4.4 → urkit-0.4.6}/pyproject.toml +1 -1
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/__init__.py +1 -1
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/cli/points.py +2 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/cli/teach.py +103 -40
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/cli/terminal.py +108 -1
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/motion.py +36 -6
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit.egg-info/PKG-INFO +1 -1
- {urkit-0.4.4 → urkit-0.4.6}/README.md +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/setup.cfg +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/__main__.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/cli/__init__.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/cli/colors.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/cli/connection_monitor.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/cli/init.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/config.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/connection.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/exceptions.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/geometry.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/gripper/__init__.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/gripper/base.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/gripper/digital.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/gripper/presets.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/gripper/robotiq.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/gripper/robotiq_preamble.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/io.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/points.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/robot.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit/telemetry.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit.egg-info/SOURCES.txt +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit.egg-info/dependency_links.txt +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit.egg-info/entry_points.txt +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit.egg-info/requires.txt +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/src/urkit.egg-info/top_level.txt +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/tests/test_exceptions.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/tests/test_geometry.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/tests/test_gripper.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/tests/test_gripper_factory.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/tests/test_gripper_presets.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/tests/test_move_sequence.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/tests/test_points.py +0 -0
- {urkit-0.4.4 → urkit-0.4.6}/tests/test_robot_integration.py +0 -0
|
@@ -16,6 +16,7 @@ from pathlib import Path
|
|
|
16
16
|
from rich.console import Console
|
|
17
17
|
from rich.table import Table
|
|
18
18
|
|
|
19
|
+
from urkit import __version__
|
|
19
20
|
from urkit.config import _load_config as load_config
|
|
20
21
|
from urkit.cli.colors import blue, cyan, dim, yellow
|
|
21
22
|
from urkit.cli.terminal import RawTerminal, read_burst, wait_input, warn_if_windows
|
|
@@ -158,6 +159,7 @@ def _interactive_points_filter(points_db: Points, all_points: list[str], points_
|
|
|
158
159
|
# Clear screen and draw header
|
|
159
160
|
sys.stdout.write("\033[2J\033[1;1H")
|
|
160
161
|
sys.stdout.write(cyan(" === POINT EXPLORER ===") + "\n")
|
|
162
|
+
sys.stdout.write(dim(f" urkit {__version__}") + "\n")
|
|
161
163
|
sys.stdout.write(dim(" Type to search · ↑↓ scroll · Enter reload · ESC quit") + "\n\n")
|
|
162
164
|
sys.stdout.write(f" {blue('Search:')} {filter_str}\n")
|
|
163
165
|
if refresh_error:
|
|
@@ -19,14 +19,19 @@ import os
|
|
|
19
19
|
import signal
|
|
20
20
|
import sys
|
|
21
21
|
import time
|
|
22
|
+
import traceback
|
|
22
23
|
from pathlib import Path
|
|
23
24
|
|
|
24
25
|
import yaml
|
|
25
26
|
|
|
27
|
+
from urkit import __version__
|
|
26
28
|
from urkit.cli.colors import blue, cyan, dim, green, red, yellow
|
|
27
29
|
from urkit.cli.terminal import (
|
|
28
30
|
RawTerminal,
|
|
31
|
+
input_active,
|
|
29
32
|
read_burst,
|
|
33
|
+
start_interrupt_watcher,
|
|
34
|
+
stop_interrupt_watcher,
|
|
30
35
|
wait_input,
|
|
31
36
|
warn_if_windows,
|
|
32
37
|
)
|
|
@@ -295,6 +300,7 @@ def _draw_screen(
|
|
|
295
300
|
# Header
|
|
296
301
|
lines.append(dim("=" * width))
|
|
297
302
|
lines.append(cyan(" === URKit Teach Pendant ===").center(width))
|
|
303
|
+
lines.append(dim(f" urkit {__version__}").center(width))
|
|
298
304
|
lines.append(cyan(f" IP: {robot.ip}").center(width))
|
|
299
305
|
if expert_mode:
|
|
300
306
|
lines.append(yellow(" EXPERT MODE").center(width))
|
|
@@ -475,6 +481,7 @@ def _read_input(prompt: str, max_len: int = 30) -> str | None:
|
|
|
475
481
|
sys.stdout.flush()
|
|
476
482
|
|
|
477
483
|
name = ""
|
|
484
|
+
input_active.set()
|
|
478
485
|
try:
|
|
479
486
|
while True:
|
|
480
487
|
if not wait_input(0.1):
|
|
@@ -511,6 +518,7 @@ def _read_input(prompt: str, max_len: int = 30) -> str | None:
|
|
|
511
518
|
break
|
|
512
519
|
finally:
|
|
513
520
|
raw.disable()
|
|
521
|
+
input_active.clear()
|
|
514
522
|
|
|
515
523
|
sys.stdout.write("\r\033[K\n")
|
|
516
524
|
sys.stdout.flush()
|
|
@@ -624,7 +632,11 @@ def _filter_select_points(
|
|
|
624
632
|
|
|
625
633
|
# Wait for input, then read the full burst (e.g. the
|
|
626
634
|
# 3-byte \x1b[A arrow-key sequence) in one go.
|
|
627
|
-
|
|
635
|
+
input_active.set()
|
|
636
|
+
try:
|
|
637
|
+
raw_bytes = read_burst(0.1)
|
|
638
|
+
finally:
|
|
639
|
+
input_active.clear()
|
|
628
640
|
if not raw_bytes:
|
|
629
641
|
if _cli_monitor and _cli_monitor.fault_detected:
|
|
630
642
|
raise URKitConnectionError(
|
|
@@ -739,11 +751,15 @@ def _submenu_goto_point(
|
|
|
739
751
|
robot, name, mode_label, start_pose, target_pose, is_cartesian, max_progress
|
|
740
752
|
)
|
|
741
753
|
max_progress = pct
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
if
|
|
745
|
-
|
|
746
|
-
|
|
754
|
+
input_active.set()
|
|
755
|
+
try:
|
|
756
|
+
if wait_input(0.05):
|
|
757
|
+
key = read_burst(0.0).decode("ascii", errors="replace")
|
|
758
|
+
if " " in key: # Space to cancel
|
|
759
|
+
cancelled = True
|
|
760
|
+
break
|
|
761
|
+
finally:
|
|
762
|
+
input_active.clear()
|
|
747
763
|
|
|
748
764
|
# Clear moving screen
|
|
749
765
|
sys.stdout.write("\033[2J\033[1;1H")
|
|
@@ -761,6 +777,9 @@ def _submenu_goto_point(
|
|
|
761
777
|
except URKitConnectionError:
|
|
762
778
|
raise
|
|
763
779
|
except Exception as e:
|
|
780
|
+
logger.debug(
|
|
781
|
+
"goto move failed: %s\n%s", e, traceback.format_exc()
|
|
782
|
+
)
|
|
764
783
|
messages.append(f"Error: {e}")
|
|
765
784
|
|
|
766
785
|
|
|
@@ -903,7 +922,11 @@ def _submenu_orient_tcp(
|
|
|
903
922
|
sys.stdout.flush()
|
|
904
923
|
needs_redraw = False
|
|
905
924
|
|
|
906
|
-
|
|
925
|
+
input_active.set()
|
|
926
|
+
try:
|
|
927
|
+
raw_bytes = read_burst(0.1)
|
|
928
|
+
finally:
|
|
929
|
+
input_active.clear()
|
|
907
930
|
if not raw_bytes:
|
|
908
931
|
if _cli_monitor and _cli_monitor.fault_detected:
|
|
909
932
|
raise URKitConnectionError(
|
|
@@ -1022,7 +1045,11 @@ def _submenu_freedrive_axes(
|
|
|
1022
1045
|
sys.stdout.flush()
|
|
1023
1046
|
needs_redraw = False
|
|
1024
1047
|
|
|
1025
|
-
|
|
1048
|
+
input_active.set()
|
|
1049
|
+
try:
|
|
1050
|
+
raw_bytes = read_burst(0.1)
|
|
1051
|
+
finally:
|
|
1052
|
+
input_active.clear()
|
|
1026
1053
|
if not raw_bytes:
|
|
1027
1054
|
continue
|
|
1028
1055
|
text = raw_bytes.decode("ascii", errors="replace")
|
|
@@ -1183,12 +1210,12 @@ def _teach_pendant(
|
|
|
1183
1210
|
print("Error: stdin is not a terminal. Run from an interactive shell.")
|
|
1184
1211
|
sys.exit(1)
|
|
1185
1212
|
|
|
1186
|
-
#
|
|
1187
|
-
#
|
|
1188
|
-
#
|
|
1189
|
-
#
|
|
1213
|
+
# Best-effort robot stop and terminal restore, then exit.
|
|
1214
|
+
# Runs from the SIGINT handler (Unix Ctrl+C) and from the
|
|
1215
|
+
# interrupt watcher (Windows Ctrl+C / fault while the main thread
|
|
1216
|
+
# is blocked in a robot call). os._exit() works even when blocked
|
|
1190
1217
|
# in C library code.
|
|
1191
|
-
def
|
|
1218
|
+
def _interrupt_exit(reason: str) -> None:
|
|
1192
1219
|
try:
|
|
1193
1220
|
if state["freedrive"]:
|
|
1194
1221
|
robot.disable_freedrive()
|
|
@@ -1207,9 +1234,16 @@ def _teach_pendant(
|
|
|
1207
1234
|
_restore_terminal()
|
|
1208
1235
|
except Exception:
|
|
1209
1236
|
pass
|
|
1210
|
-
sys.stderr.write("\
|
|
1237
|
+
sys.stderr.write(f"\n{reason}\n")
|
|
1211
1238
|
os._exit(0)
|
|
1212
1239
|
|
|
1240
|
+
# SIGINT handler: restore terminal and exit when Ctrl+C pressed.
|
|
1241
|
+
# On Unix, raw mode keeps ISIG enabled, so Ctrl+C generates SIGINT.
|
|
1242
|
+
# On Windows, Ctrl+C arrives as a raw 0x03 byte instead (handled by
|
|
1243
|
+
# the main loop's exit key path and the interrupt watcher).
|
|
1244
|
+
def _sigint_handler(signum: int, frame: object) -> None:
|
|
1245
|
+
_interrupt_exit("Interrupted.")
|
|
1246
|
+
|
|
1213
1247
|
signal.signal(signal.SIGINT, _sigint_handler)
|
|
1214
1248
|
|
|
1215
1249
|
# Connection watchdog: detects faults and interrupts blocking calls.
|
|
@@ -1225,6 +1259,21 @@ def _teach_pendant(
|
|
|
1225
1259
|
global _cli_monitor
|
|
1226
1260
|
_cli_monitor = monitor # used by _read_input and _filter_select_points
|
|
1227
1261
|
|
|
1262
|
+
# Windows escape hatch: no SIGINT delivery and no SIGALRM, so while
|
|
1263
|
+
# the main thread is blocked in a long RTDE call (e.g. a move while
|
|
1264
|
+
# a key is held) neither Ctrl+C (a console byte) nor a detected
|
|
1265
|
+
# fault can reach it. The watcher fires the same cleanup and exit.
|
|
1266
|
+
def _watcher_fault_reason() -> str | None:
|
|
1267
|
+
if monitor.fault_detected:
|
|
1268
|
+
return (
|
|
1269
|
+
f"Robot fault detected: "
|
|
1270
|
+
f"{monitor._reason or 'RTDE connection lost'}. "
|
|
1271
|
+
"RTDE connection lost."
|
|
1272
|
+
)
|
|
1273
|
+
return None
|
|
1274
|
+
|
|
1275
|
+
start_interrupt_watcher(_interrupt_exit, _watcher_fault_reason)
|
|
1276
|
+
|
|
1228
1277
|
try:
|
|
1229
1278
|
try:
|
|
1230
1279
|
_configure_terminal()
|
|
@@ -1239,39 +1288,49 @@ def _teach_pendant(
|
|
|
1239
1288
|
command_handled = False
|
|
1240
1289
|
|
|
1241
1290
|
# --- Wait for a key ---
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1291
|
+
# input_active tells the Windows interrupt watcher to
|
|
1292
|
+
# stay out of the console queue while we read.
|
|
1293
|
+
input_active.set()
|
|
1294
|
+
try:
|
|
1295
|
+
while True:
|
|
1296
|
+
if wait_input(0.05):
|
|
1297
|
+
break
|
|
1298
|
+
|
|
1299
|
+
# Periodic refresh while in freedrive — robot may be moving by hand
|
|
1300
|
+
if state["freedrive"]:
|
|
1301
|
+
now = time.monotonic()
|
|
1302
|
+
if now - last_refresh >= _FREEDRIVE_REFRESH_S:
|
|
1303
|
+
last_refresh = now
|
|
1304
|
+
_draw_screen(robot, state, messages, expert_mode)
|
|
1305
|
+
|
|
1306
|
+
# Check for fault detected by watchdog thread
|
|
1307
|
+
if monitor.fault_detected:
|
|
1308
|
+
raise URKitConnectionError(
|
|
1309
|
+
f"Robot fault detected: {monitor._reason or 'RTDE connection lost'}. "
|
|
1310
|
+
"RTDE connection lost."
|
|
1311
|
+
)
|
|
1259
1312
|
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1313
|
+
# --- Read key, draining buffered repeats ---
|
|
1314
|
+
# When a key is held, the terminal auto-repeats it into the
|
|
1315
|
+
# input buffer. Reading only one character leaves stale
|
|
1316
|
+
# repeats behind — when the user switches from one
|
|
1317
|
+
# movement key to another, the last buffered copy of the
|
|
1318
|
+
# old key fires one final move in the wrong direction
|
|
1319
|
+
# before the new key is read.
|
|
1320
|
+
# Fix: drain all immediately available bytes
|
|
1321
|
+
# (non-blocking) and use the last one.
|
|
1322
|
+
key_text = read_burst(0.0).decode("ascii", errors="replace")
|
|
1323
|
+
finally:
|
|
1324
|
+
input_active.clear()
|
|
1269
1325
|
key = key_text[-1].lower() if key_text else ""
|
|
1270
1326
|
if not key:
|
|
1271
1327
|
continue
|
|
1272
1328
|
|
|
1273
1329
|
# Exit
|
|
1274
1330
|
if key == "\x1b" or key == "\x03":
|
|
1331
|
+
# The main loop owns cleanup from here; the watcher
|
|
1332
|
+
# must not fire a second stop/restore in parallel.
|
|
1333
|
+
stop_interrupt_watcher()
|
|
1275
1334
|
if state["freedrive"]:
|
|
1276
1335
|
try:
|
|
1277
1336
|
robot.disable_freedrive()
|
|
@@ -1317,6 +1376,9 @@ def _teach_pendant(
|
|
|
1317
1376
|
moved = True
|
|
1318
1377
|
last_move_t = time.monotonic()
|
|
1319
1378
|
except MotionError as e:
|
|
1379
|
+
logger.debug(
|
|
1380
|
+
"move key %r failed: %s\n%s", key, e, traceback.format_exc()
|
|
1381
|
+
)
|
|
1320
1382
|
messages.append(f"Error: {e}")
|
|
1321
1383
|
|
|
1322
1384
|
# --- Step size ---
|
|
@@ -1527,6 +1589,7 @@ def _teach_pendant(
|
|
|
1527
1589
|
print(f"\n{red('Fault:')} {e}")
|
|
1528
1590
|
finally:
|
|
1529
1591
|
# Stop monitor and restore signal handler
|
|
1592
|
+
stop_interrupt_watcher()
|
|
1530
1593
|
monitor.stop()
|
|
1531
1594
|
if _old_sigalrm is not None:
|
|
1532
1595
|
signal.signal(signal.SIGALRM, _old_sigalrm)
|
|
@@ -30,8 +30,9 @@ from __future__ import annotations
|
|
|
30
30
|
import ctypes
|
|
31
31
|
import os
|
|
32
32
|
import sys
|
|
33
|
+
import threading
|
|
33
34
|
import time
|
|
34
|
-
from typing import Any
|
|
35
|
+
from typing import Any, Callable
|
|
35
36
|
|
|
36
37
|
if sys.platform == "win32":
|
|
37
38
|
import msvcrt
|
|
@@ -126,6 +127,112 @@ if sys.platform == "win32":
|
|
|
126
127
|
pass
|
|
127
128
|
|
|
128
129
|
|
|
130
|
+
input_active = threading.Event()
|
|
131
|
+
"""Set while CLI code is actively reading stdin (input-wait loops, text
|
|
132
|
+
prompts, menus). The Windows interrupt watcher stays out of the console
|
|
133
|
+
input queue while this is set, so it can never steal a keystroke from
|
|
134
|
+
the main reader. Callers set it around their read sections and clear it
|
|
135
|
+
afterwards."""
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
watcher_stop = threading.Event()
|
|
139
|
+
"""Set when the CLI begins its own shutdown (normal exit, fault exit,
|
|
140
|
+
Ctrl+C in the main loop). The Windows interrupt watcher exits quietly
|
|
141
|
+
without firing, so it cannot race the main thread's cleanup (double
|
|
142
|
+
robot stop, double terminal restore)."""
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def stop_interrupt_watcher() -> None:
|
|
146
|
+
"""Tell the interrupt watcher to exit quietly. No-op on non-Windows."""
|
|
147
|
+
watcher_stop.set()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def start_interrupt_watcher(
|
|
151
|
+
on_fire: Callable[[str], None],
|
|
152
|
+
fault_reason: Callable[[], str | None],
|
|
153
|
+
) -> threading.Thread | None:
|
|
154
|
+
"""Start the Windows-only interrupt watcher. Returns None elsewhere.
|
|
155
|
+
|
|
156
|
+
On Windows, while the main thread is blocked in a long RTDE call
|
|
157
|
+
(not reading input) nothing can interrupt it: Ctrl+C is a raw
|
|
158
|
+
console byte that only the input loops read, and there is no
|
|
159
|
+
SIGALRM. This daemon thread covers that gap. It polls, only while
|
|
160
|
+
input_active is clear:
|
|
161
|
+
|
|
162
|
+
- fault_reason() for a connection/robot fault
|
|
163
|
+
- the console for a Ctrl+C (0x03) byte; any other byte is pushed
|
|
164
|
+
back with ungetch() so the main reader still sees it, and the
|
|
165
|
+
watcher backs off to avoid interfering with resumed input
|
|
166
|
+
|
|
167
|
+
When either fires it calls on_fire(reason) exactly once; the
|
|
168
|
+
caller should clean up (stop robot, restore terminal) and exit.
|
|
169
|
+
"""
|
|
170
|
+
if sys.platform != "win32":
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
# Only reachable on Windows (guard above), so this never runs on
|
|
174
|
+
# Linux. Local import because mypy prunes the top-level platform
|
|
175
|
+
# guard and cannot see the module inside this closure.
|
|
176
|
+
import msvcrt
|
|
177
|
+
|
|
178
|
+
def _where_blocked() -> None:
|
|
179
|
+
# Diagnostic: report where the main thread was stuck so the
|
|
180
|
+
# next occurrence can be diagnosed (usually a blocking socket
|
|
181
|
+
# read inside ur_rtde with no timeout).
|
|
182
|
+
frame = sys._current_frames().get(threading.main_thread().ident)
|
|
183
|
+
if frame is None:
|
|
184
|
+
return
|
|
185
|
+
code = frame.f_code
|
|
186
|
+
sys.stderr.write(
|
|
187
|
+
"[urkit] main thread blocked in "
|
|
188
|
+
f"{os.path.basename(code.co_filename)}:{frame.f_lineno} "
|
|
189
|
+
f"in {code.co_name}\n"
|
|
190
|
+
)
|
|
191
|
+
sys.stderr.flush()
|
|
192
|
+
|
|
193
|
+
def _run() -> None:
|
|
194
|
+
backoff = 0.0
|
|
195
|
+
while not watcher_stop.is_set():
|
|
196
|
+
if backoff > 0:
|
|
197
|
+
time.sleep(backoff)
|
|
198
|
+
backoff = 0.0
|
|
199
|
+
if input_active.is_set():
|
|
200
|
+
time.sleep(0.05)
|
|
201
|
+
continue
|
|
202
|
+
reason = fault_reason()
|
|
203
|
+
if reason is not None:
|
|
204
|
+
_where_blocked()
|
|
205
|
+
on_fire(reason)
|
|
206
|
+
return
|
|
207
|
+
try:
|
|
208
|
+
if not msvcrt.kbhit():
|
|
209
|
+
time.sleep(0.05)
|
|
210
|
+
continue
|
|
211
|
+
byte = msvcrt.getch()
|
|
212
|
+
except OSError:
|
|
213
|
+
# Console is closing (shutdown in progress). The watcher
|
|
214
|
+
# is best-effort; exit quietly instead of dying with a
|
|
215
|
+
# traceback that races the main thread's exit.
|
|
216
|
+
return
|
|
217
|
+
if byte == b"\x03":
|
|
218
|
+
_where_blocked()
|
|
219
|
+
on_fire("Interrupted.")
|
|
220
|
+
return
|
|
221
|
+
# Not Ctrl+C: give the byte back and back off so the main
|
|
222
|
+
# reader (resuming from a blocking call) processes it.
|
|
223
|
+
try:
|
|
224
|
+
msvcrt.ungetch(byte[0])
|
|
225
|
+
except OSError:
|
|
226
|
+
return
|
|
227
|
+
backoff = 0.2
|
|
228
|
+
|
|
229
|
+
watcher = threading.Thread(
|
|
230
|
+
target=_run, daemon=True, name="interrupt-watcher"
|
|
231
|
+
)
|
|
232
|
+
watcher.start()
|
|
233
|
+
return watcher
|
|
234
|
+
|
|
235
|
+
|
|
129
236
|
def warn_if_windows() -> None:
|
|
130
237
|
"""Print a notice when the interactive CLI runs on Windows.
|
|
131
238
|
|
|
@@ -30,17 +30,26 @@ def _suppress_rtde_stderr() -> Iterator[None]:
|
|
|
30
30
|
to raw stderr (fd 2) on every call when the script is stopped.
|
|
31
31
|
This context manager suppresses those messages by redirecting fd 2
|
|
32
32
|
around the call.
|
|
33
|
+
|
|
34
|
+
Diagnostic: set URKIT_RTDE_STDERR=<path> to capture the suppressed
|
|
35
|
+
output in a file (append mode) instead. ur_rtde move failures are
|
|
36
|
+
reported there and nowhere else, so this is the only way to see
|
|
37
|
+
why a move was rejected.
|
|
33
38
|
"""
|
|
34
|
-
|
|
39
|
+
capture_path = os.environ.get("URKIT_RTDE_STDERR")
|
|
40
|
+
if capture_path:
|
|
41
|
+
target_fd = os.open(capture_path, os.O_WRONLY | os.O_CREAT | os.O_APPEND)
|
|
42
|
+
else:
|
|
43
|
+
target_fd = os.open(os.devnull, os.O_WRONLY)
|
|
35
44
|
old_stderr = os.dup(2)
|
|
36
45
|
try:
|
|
37
|
-
os.dup2(
|
|
46
|
+
os.dup2(target_fd, 2)
|
|
38
47
|
sys.stderr.flush()
|
|
39
48
|
yield
|
|
40
49
|
finally:
|
|
41
50
|
os.dup2(old_stderr, 2)
|
|
42
51
|
os.close(old_stderr)
|
|
43
|
-
os.close(
|
|
52
|
+
os.close(target_fd)
|
|
44
53
|
|
|
45
54
|
if TYPE_CHECKING:
|
|
46
55
|
from rtde_control import RTDEControlInterface
|
|
@@ -151,7 +160,14 @@ class Motion:
|
|
|
151
160
|
"movej: joints=%s, vel=%.3f, acc=%.3f, async=%s", joints, vel, acc, asynchronous
|
|
152
161
|
)
|
|
153
162
|
with _suppress_rtde_stderr():
|
|
154
|
-
self._rtde_c.moveJ(joints, vel, acc, asynchronous=asynchronous)
|
|
163
|
+
ok = self._rtde_c.moveJ(joints, vel, acc, asynchronous=asynchronous)
|
|
164
|
+
if ok is False:
|
|
165
|
+
raise MotionError(
|
|
166
|
+
"moveJ rejected by the controller: the RTDE control script "
|
|
167
|
+
"stopped, the robot entered a stop state, or the command "
|
|
168
|
+
"timed out. Set URKIT_RTDE_STDERR=<file> to capture "
|
|
169
|
+
"ur_rtde's error output."
|
|
170
|
+
)
|
|
155
171
|
except MotionError:
|
|
156
172
|
raise
|
|
157
173
|
except Exception as e:
|
|
@@ -197,7 +213,14 @@ class Motion:
|
|
|
197
213
|
"movel: pose=%s, vel=%.3f, acc=%.3f, async=%s", pose, vel, acc, asynchronous
|
|
198
214
|
)
|
|
199
215
|
with _suppress_rtde_stderr():
|
|
200
|
-
self._rtde_c.moveL(pose, vel, acc, asynchronous=asynchronous)
|
|
216
|
+
ok = self._rtde_c.moveL(pose, vel, acc, asynchronous=asynchronous)
|
|
217
|
+
if ok is False:
|
|
218
|
+
raise MotionError(
|
|
219
|
+
"moveL rejected by the controller: the RTDE control script "
|
|
220
|
+
"stopped, the robot entered a stop state, or the command "
|
|
221
|
+
"timed out. Set URKIT_RTDE_STDERR=<file> to capture "
|
|
222
|
+
"ur_rtde's error output."
|
|
223
|
+
)
|
|
201
224
|
except MotionError:
|
|
202
225
|
raise
|
|
203
226
|
except Exception as e:
|
|
@@ -249,7 +272,14 @@ class Motion:
|
|
|
249
272
|
current, target, frame.name, vel, acc,
|
|
250
273
|
)
|
|
251
274
|
with _suppress_rtde_stderr():
|
|
252
|
-
self._rtde_c.moveL(target, vel, acc)
|
|
275
|
+
ok = self._rtde_c.moveL(target, vel, acc)
|
|
276
|
+
if ok is False:
|
|
277
|
+
raise MotionError(
|
|
278
|
+
"moveL rejected by the controller: the RTDE control script "
|
|
279
|
+
"stopped, the robot entered a stop state, or the command "
|
|
280
|
+
"timed out. Set URKIT_RTDE_STDERR=<file> to capture "
|
|
281
|
+
"ur_rtde's error output."
|
|
282
|
+
)
|
|
253
283
|
except MotionError:
|
|
254
284
|
raise
|
|
255
285
|
except Exception as e:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|