urkit 0.4.1__tar.gz → 0.4.2__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.1 → urkit-0.4.2}/PKG-INFO +7 -2
- {urkit-0.4.1 → urkit-0.4.2}/README.md +6 -1
- {urkit-0.4.1 → urkit-0.4.2}/pyproject.toml +1 -1
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/__init__.py +1 -1
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/cli/connection_monitor.py +8 -1
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/cli/points.py +28 -35
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/cli/teach.py +100 -154
- urkit-0.4.2/src/urkit/cli/terminal.py +233 -0
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/points.py +8 -5
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/robot.py +7 -6
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit.egg-info/PKG-INFO +7 -2
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit.egg-info/SOURCES.txt +1 -0
- {urkit-0.4.1 → urkit-0.4.2}/setup.cfg +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/__main__.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/cli/__init__.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/cli/colors.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/cli/init.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/config.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/connection.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/exceptions.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/geometry.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/gripper/__init__.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/gripper/base.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/gripper/digital.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/gripper/presets.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/gripper/robotiq.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/gripper/robotiq_preamble.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/io.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/motion.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit/telemetry.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit.egg-info/dependency_links.txt +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit.egg-info/entry_points.txt +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit.egg-info/requires.txt +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/src/urkit.egg-info/top_level.txt +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/tests/test_exceptions.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/tests/test_geometry.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/tests/test_gripper.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/tests/test_gripper_factory.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/tests/test_gripper_presets.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/tests/test_move_sequence.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/tests/test_points.py +0 -0
- {urkit-0.4.1 → urkit-0.4.2}/tests/test_robot_integration.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: urkit
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Universal Robots e-Series control toolkit built on ur_rtde
|
|
5
5
|
Author: URKit Contributors
|
|
6
6
|
License: MIT
|
|
@@ -94,6 +94,11 @@ The `-U` (upgrade) flag ensures you always get the latest version. This project
|
|
|
94
94
|
|
|
95
95
|
Requires Python 3.8+ and a Universal Robots e-Series (UR3e to UR30).
|
|
96
96
|
|
|
97
|
+
### Platform Support
|
|
98
|
+
|
|
99
|
+
- **Linux, macOS**: fully supported
|
|
100
|
+
- **Windows**: the Python library works on Windows. The interactive CLIs (`urkit teach`, `urkit points`) run on Windows but are **not fully tested and not officially supported**. A warning is printed at startup. If you hit input or console problems, use Linux, macOS, or WSL2
|
|
101
|
+
|
|
97
102
|
### Robot Setup (one-time)
|
|
98
103
|
|
|
99
104
|
1. **Network**: Connect your PC to the robot via Ethernet cable (direct) or a switch.
|
|
@@ -663,7 +668,7 @@ Default is `None` (controller handles IK natively). Set it globally when most of
|
|
|
663
668
|
|
|
664
669
|
#### Point Management
|
|
665
670
|
|
|
666
|
-
Points are stored in the
|
|
671
|
+
Points are stored as absolute TCP poses in the robot base frame. When you move to a point, the controller moves whatever TCP is currently active to that base-frame pose, so the tool tip lands at the same physical location. Change the TCP offset (or swap grippers) and your saved points stay valid.
|
|
667
672
|
|
|
668
673
|
```python
|
|
669
674
|
robot.save_point("here")
|
|
@@ -68,6 +68,11 @@ The `-U` (upgrade) flag ensures you always get the latest version. This project
|
|
|
68
68
|
|
|
69
69
|
Requires Python 3.8+ and a Universal Robots e-Series (UR3e to UR30).
|
|
70
70
|
|
|
71
|
+
### Platform Support
|
|
72
|
+
|
|
73
|
+
- **Linux, macOS**: fully supported
|
|
74
|
+
- **Windows**: the Python library works on Windows. The interactive CLIs (`urkit teach`, `urkit points`) run on Windows but are **not fully tested and not officially supported**. A warning is printed at startup. If you hit input or console problems, use Linux, macOS, or WSL2
|
|
75
|
+
|
|
71
76
|
### Robot Setup (one-time)
|
|
72
77
|
|
|
73
78
|
1. **Network**: Connect your PC to the robot via Ethernet cable (direct) or a switch.
|
|
@@ -637,7 +642,7 @@ Default is `None` (controller handles IK natively). Set it globally when most of
|
|
|
637
642
|
|
|
638
643
|
#### Point Management
|
|
639
644
|
|
|
640
|
-
Points are stored in the
|
|
645
|
+
Points are stored as absolute TCP poses in the robot base frame. When you move to a point, the controller moves whatever TCP is currently active to that base-frame pose, so the tool tip lands at the same physical location. Change the TCP offset (or swap grippers) and your saved points stay valid.
|
|
641
646
|
|
|
642
647
|
```python
|
|
643
648
|
robot.save_point("here")
|
|
@@ -144,10 +144,17 @@ class ConnectionMonitor:
|
|
|
144
144
|
logger.info("Connection monitor loop exited")
|
|
145
145
|
|
|
146
146
|
def _trigger(self, reason: str) -> None:
|
|
147
|
-
"""Record fault reason and send SIGALRM to interrupt blocking calls.
|
|
147
|
+
"""Record fault reason and send SIGALRM to interrupt blocking calls.
|
|
148
|
+
|
|
149
|
+
On Windows there is no self-signal: the fault_detected flag is
|
|
150
|
+
the only interruption path, and callers poll it between and
|
|
151
|
+
after RTDE calls.
|
|
152
|
+
"""
|
|
148
153
|
self._reason = reason
|
|
149
154
|
self._fault_event.set()
|
|
150
155
|
logger.info("Fault detected: %s", reason)
|
|
156
|
+
if not hasattr(signal, "SIGALRM"):
|
|
157
|
+
return
|
|
151
158
|
try:
|
|
152
159
|
os.kill(os.getpid(), signal.SIGALRM)
|
|
153
160
|
except OSError as e:
|
|
@@ -9,9 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import difflib
|
|
11
11
|
import io
|
|
12
|
-
import select
|
|
13
12
|
import sys
|
|
14
|
-
import termios
|
|
15
13
|
import time
|
|
16
14
|
from pathlib import Path
|
|
17
15
|
|
|
@@ -20,6 +18,7 @@ from rich.table import Table
|
|
|
20
18
|
|
|
21
19
|
from urkit.config import _load_config as load_config
|
|
22
20
|
from urkit.cli.colors import blue, cyan, dim, yellow
|
|
21
|
+
from urkit.cli.terminal import RawTerminal, read_burst, wait_input, warn_if_windows
|
|
23
22
|
from urkit.points import Points
|
|
24
23
|
|
|
25
24
|
|
|
@@ -29,6 +28,7 @@ def points_command(args) -> None:
|
|
|
29
28
|
Args:
|
|
30
29
|
args: Parsed arguments from argparse (with points subcommand attributes).
|
|
31
30
|
"""
|
|
31
|
+
warn_if_windows()
|
|
32
32
|
_explore_points(args)
|
|
33
33
|
|
|
34
34
|
|
|
@@ -120,12 +120,8 @@ def _interactive_points_filter(points_db: Points, all_points: list[str], points_
|
|
|
120
120
|
refresh_error = None
|
|
121
121
|
|
|
122
122
|
# Set terminal to raw mode
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
new_settings[3] = new_settings[3] & ~(termios.ICANON | termios.ECHO)
|
|
126
|
-
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, new_settings)
|
|
127
|
-
|
|
128
|
-
fd = sys.stdin.fileno()
|
|
123
|
+
raw = RawTerminal()
|
|
124
|
+
raw.enable()
|
|
129
125
|
|
|
130
126
|
try:
|
|
131
127
|
while True:
|
|
@@ -238,44 +234,41 @@ def _interactive_points_filter(points_db: Points, all_points: list[str], points_
|
|
|
238
234
|
last_refresh = time.time()
|
|
239
235
|
|
|
240
236
|
# Check for input (non-blocking to allow periodic refresh)
|
|
241
|
-
|
|
242
|
-
if not rlist:
|
|
237
|
+
if not wait_input(0.1):
|
|
243
238
|
continue
|
|
244
239
|
|
|
245
|
-
# Read input
|
|
240
|
+
# Read the full input burst. On Unix an arrow key arrives as
|
|
241
|
+
# \x1b[A in the same burst; on Windows the terminal shim
|
|
242
|
+
# translates arrow keys to the same byte sequence.
|
|
246
243
|
try:
|
|
247
|
-
|
|
244
|
+
text = read_burst(0.0).decode("ascii", errors="replace")
|
|
248
245
|
except Exception:
|
|
249
246
|
break
|
|
247
|
+
if not text:
|
|
248
|
+
continue
|
|
250
249
|
|
|
251
|
-
if
|
|
252
|
-
#
|
|
253
|
-
|
|
254
|
-
if
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
elif ch3 == "B": # Down arrow - scroll down
|
|
264
|
-
filtered = [p for p in all_points_sorted if filter_str == "" or filter_str.lower() in p.lower()]
|
|
265
|
-
scroll = min(len(filtered) - 1, scroll + 1) if filtered else 0
|
|
266
|
-
needs_redraw = True
|
|
267
|
-
except Exception:
|
|
268
|
-
break
|
|
250
|
+
if text.startswith("\x1b"): # ESC or arrow key or function key
|
|
251
|
+
# A bare ESC may still be the first byte of a sequence
|
|
252
|
+
# that arrives late; wait a bit before treating it as quit.
|
|
253
|
+
if text == "\x1b":
|
|
254
|
+
text = text + read_burst(0.2).decode("ascii", errors="replace")
|
|
255
|
+
if text.endswith("[A"): # Up arrow - scroll up
|
|
256
|
+
scroll = max(0, scroll - 1)
|
|
257
|
+
needs_redraw = True
|
|
258
|
+
elif text.endswith("[B"): # Down arrow - scroll down
|
|
259
|
+
filtered = [p for p in all_points_sorted if filter_str == "" or filter_str.lower() in p.lower()]
|
|
260
|
+
scroll = min(len(filtered) - 1, scroll + 1) if filtered else 0
|
|
261
|
+
needs_redraw = True
|
|
269
262
|
else:
|
|
270
|
-
#
|
|
263
|
+
# Just ESC (or an unrecognized sequence) — quit
|
|
271
264
|
break
|
|
272
|
-
elif
|
|
265
|
+
elif text == "\x7f" or text == "\x08": # Backspace
|
|
273
266
|
if filter_str:
|
|
274
267
|
filter_str = filter_str[:-1]
|
|
275
268
|
scroll = 0
|
|
276
269
|
needs_redraw = True
|
|
277
|
-
elif
|
|
278
|
-
filter_str +=
|
|
270
|
+
elif text.isprintable():
|
|
271
|
+
filter_str += text
|
|
279
272
|
scroll = 0
|
|
280
273
|
needs_redraw = True
|
|
281
274
|
|
|
@@ -283,7 +276,7 @@ def _interactive_points_filter(points_db: Points, all_points: list[str], points_
|
|
|
283
276
|
# Ctrl+C — exit gracefully without traceback
|
|
284
277
|
pass
|
|
285
278
|
finally:
|
|
286
|
-
|
|
279
|
+
raw.disable()
|
|
287
280
|
sys.stdout.write("\n")
|
|
288
281
|
|
|
289
282
|
|
|
@@ -16,18 +16,21 @@ import ipaddress
|
|
|
16
16
|
import logging
|
|
17
17
|
import math
|
|
18
18
|
import os
|
|
19
|
-
import select
|
|
20
19
|
import signal
|
|
21
20
|
import sys
|
|
22
|
-
import termios
|
|
23
21
|
import time
|
|
24
|
-
import tty
|
|
25
22
|
from pathlib import Path
|
|
26
23
|
|
|
27
24
|
import yaml
|
|
28
25
|
|
|
29
|
-
from urkit.config import _load_config as load_config
|
|
30
26
|
from urkit.cli.colors import blue, cyan, dim, green, red, yellow
|
|
27
|
+
from urkit.cli.terminal import (
|
|
28
|
+
RawTerminal,
|
|
29
|
+
read_burst,
|
|
30
|
+
wait_input,
|
|
31
|
+
warn_if_windows,
|
|
32
|
+
)
|
|
33
|
+
from urkit.config import _load_config as load_config
|
|
31
34
|
from urkit.cli.connection_monitor import ConnectionMonitor
|
|
32
35
|
from urkit.cli.points import _interactive_points_filter
|
|
33
36
|
from urkit.connection import (
|
|
@@ -181,38 +184,6 @@ def _validate_ip(ip: str) -> bool:
|
|
|
181
184
|
|
|
182
185
|
|
|
183
186
|
|
|
184
|
-
# ------------------------------------------------------------------
|
|
185
|
-
# Raw terminal I/O
|
|
186
|
-
# ------------------------------------------------------------------
|
|
187
|
-
|
|
188
|
-
class _RawTerminal:
|
|
189
|
-
"""Manage raw terminal mode with tcgetattr/tcsetattr.
|
|
190
|
-
|
|
191
|
-
Context-manager based: enters raw mode on __enter__, restores on __exit__.
|
|
192
|
-
Single-character non-blocking reads via getkey().
|
|
193
|
-
"""
|
|
194
|
-
|
|
195
|
-
def __enter__(self) -> "_RawTerminal":
|
|
196
|
-
self._old_settings = termios.tcgetattr(sys.stdin)
|
|
197
|
-
tty.setcbreak(sys.stdin.fileno())
|
|
198
|
-
return self
|
|
199
|
-
|
|
200
|
-
def __exit__(self, *args) -> None:
|
|
201
|
-
if hasattr(self, "_old_settings"):
|
|
202
|
-
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self._old_settings)
|
|
203
|
-
|
|
204
|
-
def getkey(self) -> str:
|
|
205
|
-
"""Read a single character without waiting for Enter.
|
|
206
|
-
|
|
207
|
-
Returns:
|
|
208
|
-
Single character string, or empty string if no input.
|
|
209
|
-
"""
|
|
210
|
-
ready, _, _ = select.select([sys.stdin], [], [], 0.05)
|
|
211
|
-
if ready:
|
|
212
|
-
return sys.stdin.read(1)
|
|
213
|
-
return ""
|
|
214
|
-
|
|
215
|
-
|
|
216
187
|
# ------------------------------------------------------------------
|
|
217
188
|
# UI rendering
|
|
218
189
|
# ------------------------------------------------------------------
|
|
@@ -469,16 +440,19 @@ def _draw_help() -> None:
|
|
|
469
440
|
# Point submenus (restore canonical terminal for multi-char input)
|
|
470
441
|
# ------------------------------------------------------------------
|
|
471
442
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
return old_settings
|
|
443
|
+
# Raw terminal shared by the main loop, the SIGINT handler, and the
|
|
444
|
+
# exit path. All terminal setup goes through the cross-platform shim.
|
|
445
|
+
_raw_terminal = RawTerminal()
|
|
446
|
+
|
|
477
447
|
|
|
448
|
+
def _configure_terminal() -> None:
|
|
449
|
+
"""Enter raw terminal mode (no canonical, no echo)."""
|
|
450
|
+
_raw_terminal.enable()
|
|
478
451
|
|
|
479
|
-
|
|
452
|
+
|
|
453
|
+
def _restore_terminal() -> None:
|
|
480
454
|
"""Restore canonical mode and echo."""
|
|
481
|
-
|
|
455
|
+
_raw_terminal.disable()
|
|
482
456
|
|
|
483
457
|
|
|
484
458
|
def _read_input(prompt: str, max_len: int = 30) -> str | None:
|
|
@@ -494,51 +468,50 @@ def _read_input(prompt: str, max_len: int = 30) -> str | None:
|
|
|
494
468
|
Returns:
|
|
495
469
|
Input string, or None if cancelled (ESC/empty).
|
|
496
470
|
"""
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
# Disable echo — we handle character display ourselves to avoid conflicts
|
|
500
|
-
settings = termios.tcgetattr(sys.stdin)
|
|
501
|
-
settings[3] = settings[3] & ~termios.ECHO
|
|
502
|
-
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, settings)
|
|
471
|
+
raw = RawTerminal()
|
|
472
|
+
raw.enable()
|
|
503
473
|
|
|
504
474
|
sys.stdout.write(prompt)
|
|
505
475
|
sys.stdout.flush()
|
|
506
476
|
|
|
507
477
|
name = ""
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
478
|
+
try:
|
|
479
|
+
while True:
|
|
480
|
+
if not wait_input(0.1):
|
|
481
|
+
if _cli_monitor and _cli_monitor.fault_detected:
|
|
482
|
+
raise URKitConnectionError(
|
|
483
|
+
f"Robot fault detected: {_cli_monitor._reason or 'RTDE connection lost'}. "
|
|
484
|
+
"RTDE connection lost."
|
|
485
|
+
)
|
|
486
|
+
continue
|
|
487
|
+
text = read_burst(0.0).decode("ascii", errors="replace")
|
|
488
|
+
if not text:
|
|
489
|
+
continue
|
|
490
|
+
done = False
|
|
491
|
+
for ch in text:
|
|
492
|
+
if ch == "\x1b" or ch == "\x03": # ESC or Ctrl+C → cancel
|
|
493
|
+
sys.stdout.write("\033[K")
|
|
494
|
+
sys.stdout.flush()
|
|
495
|
+
return None
|
|
496
|
+
elif ch == "\x08" or ch == "\x7f": # Backspace
|
|
497
|
+
if name:
|
|
498
|
+
name = name[:-1]
|
|
499
|
+
sys.stdout.write("\b \b")
|
|
500
|
+
sys.stdout.flush()
|
|
501
|
+
elif ch == "\n" or ch == "\r":
|
|
502
|
+
sys.stdout.write("\033[K")
|
|
503
|
+
sys.stdout.flush()
|
|
504
|
+
done = True
|
|
505
|
+
break
|
|
506
|
+
elif len(name) < max_len and ch.isprintable():
|
|
507
|
+
name += ch
|
|
508
|
+
sys.stdout.write(ch)
|
|
509
|
+
sys.stdout.flush()
|
|
510
|
+
if done:
|
|
511
|
+
break
|
|
512
|
+
finally:
|
|
513
|
+
raw.disable()
|
|
540
514
|
|
|
541
|
-
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
|
|
542
515
|
sys.stdout.write("\r\033[K\n")
|
|
543
516
|
sys.stdout.flush()
|
|
544
517
|
return name.strip()
|
|
@@ -606,12 +579,8 @@ def _filter_select_points(
|
|
|
606
579
|
cursor = 0
|
|
607
580
|
|
|
608
581
|
# Set terminal to raw mode once, outside the loop
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
new_settings[3] = new_settings[3] & ~(termios.ICANON | termios.ECHO)
|
|
612
|
-
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, new_settings)
|
|
613
|
-
|
|
614
|
-
fd = sys.stdin.fileno()
|
|
582
|
+
raw = RawTerminal()
|
|
583
|
+
raw.enable()
|
|
615
584
|
|
|
616
585
|
try:
|
|
617
586
|
while True:
|
|
@@ -645,27 +614,19 @@ def _filter_select_points(
|
|
|
645
614
|
|
|
646
615
|
sys.stdout.flush()
|
|
647
616
|
|
|
648
|
-
|
|
649
|
-
|
|
617
|
+
# Wait for input, then read the full burst (e.g. the
|
|
618
|
+
# 3-byte \x1b[A arrow-key sequence) in one go.
|
|
619
|
+
raw_bytes = read_burst(0.1)
|
|
620
|
+
if not raw_bytes:
|
|
650
621
|
if _cli_monitor and _cli_monitor.fault_detected:
|
|
651
|
-
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
|
|
652
622
|
raise URKitConnectionError(
|
|
653
623
|
f"Robot fault detected: {_cli_monitor._reason or 'RTDE connection lost'}. "
|
|
654
624
|
"RTDE connection lost."
|
|
655
625
|
)
|
|
656
626
|
continue
|
|
657
627
|
|
|
658
|
-
# Read all bytes that arrived together — avoids Python text-mode
|
|
659
|
-
# buffering silently consuming escape sequence bytes.
|
|
660
|
-
# After select() says ready, os.read() gets everything the terminal
|
|
661
|
-
# sent in one burst (e.g., \x1b[A for up-arrow). A second read
|
|
662
|
-
# would block, so we only read once per select() notification.
|
|
663
|
-
raw = os.read(fd, 64)
|
|
664
|
-
if not raw:
|
|
665
|
-
continue
|
|
666
|
-
|
|
667
628
|
# Decode and parse sequentially, handling multi-byte escape sequences
|
|
668
|
-
text =
|
|
629
|
+
text = raw_bytes.decode("ascii", errors="replace")
|
|
669
630
|
i = 0
|
|
670
631
|
while i < len(text):
|
|
671
632
|
ch = text[i]
|
|
@@ -698,7 +659,7 @@ def _filter_select_points(
|
|
|
698
659
|
|
|
699
660
|
i += 1
|
|
700
661
|
finally:
|
|
701
|
-
|
|
662
|
+
raw.disable()
|
|
702
663
|
|
|
703
664
|
|
|
704
665
|
def _submenu_goto_point(
|
|
@@ -764,10 +725,9 @@ def _submenu_goto_point(
|
|
|
764
725
|
robot, name, mode_label, start_pose, target_pose, is_cartesian, max_progress
|
|
765
726
|
)
|
|
766
727
|
max_progress = pct
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
key
|
|
770
|
-
if key == " ": # Space to cancel
|
|
728
|
+
if wait_input(0.05):
|
|
729
|
+
key = read_burst(0.0).decode("ascii", errors="replace")
|
|
730
|
+
if " " in key: # Space to cancel
|
|
771
731
|
cancelled = True
|
|
772
732
|
break
|
|
773
733
|
|
|
@@ -905,11 +865,8 @@ def _submenu_orient_tcp(
|
|
|
905
865
|
) -> None:
|
|
906
866
|
"""Interactive submenu to orient TCP along a base-axis direction."""
|
|
907
867
|
cursor = 0
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
new_settings[3] = new_settings[3] & ~(termios.ICANON | termios.ECHO)
|
|
911
|
-
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, new_settings)
|
|
912
|
-
fd = sys.stdin.fileno()
|
|
868
|
+
raw = RawTerminal()
|
|
869
|
+
raw.enable()
|
|
913
870
|
|
|
914
871
|
try:
|
|
915
872
|
while True:
|
|
@@ -926,20 +883,15 @@ def _submenu_orient_tcp(
|
|
|
926
883
|
sys.stdout.write(f"\n {dim('Enter')} to orient {green(_TCP_ORIENT_OPTIONS[cursor][0])}\n")
|
|
927
884
|
sys.stdout.flush()
|
|
928
885
|
|
|
929
|
-
|
|
930
|
-
if not
|
|
886
|
+
raw_bytes = read_burst(0.1)
|
|
887
|
+
if not raw_bytes:
|
|
931
888
|
if _cli_monitor and _cli_monitor.fault_detected:
|
|
932
|
-
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
|
|
933
889
|
raise URKitConnectionError(
|
|
934
890
|
f"Robot fault detected: {_cli_monitor._reason or 'RTDE connection lost'}. "
|
|
935
891
|
"RTDE connection lost."
|
|
936
892
|
)
|
|
937
893
|
continue
|
|
938
|
-
|
|
939
|
-
raw = os.read(fd, 64)
|
|
940
|
-
if not raw:
|
|
941
|
-
continue
|
|
942
|
-
text = raw.decode("ascii", errors="replace")
|
|
894
|
+
text = raw_bytes.decode("ascii", errors="replace")
|
|
943
895
|
i = 0
|
|
944
896
|
selected = None
|
|
945
897
|
while i < len(text):
|
|
@@ -964,7 +916,7 @@ def _submenu_orient_tcp(
|
|
|
964
916
|
if selected is not None:
|
|
965
917
|
break
|
|
966
918
|
finally:
|
|
967
|
-
|
|
919
|
+
raw.disable()
|
|
968
920
|
|
|
969
921
|
if selected is None:
|
|
970
922
|
messages.append("Cancelled")
|
|
@@ -1019,11 +971,8 @@ def _submenu_freedrive_axes(
|
|
|
1019
971
|
axes = [1, 1, 1, 1, 1, 1]
|
|
1020
972
|
|
|
1021
973
|
cursor = 0
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
new_settings[3] = new_settings[3] & ~(termios.ICANON | termios.ECHO)
|
|
1025
|
-
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, new_settings)
|
|
1026
|
-
fd = sys.stdin.fileno()
|
|
974
|
+
raw = RawTerminal()
|
|
975
|
+
raw.enable()
|
|
1027
976
|
|
|
1028
977
|
try:
|
|
1029
978
|
while True:
|
|
@@ -1043,14 +992,10 @@ def _submenu_freedrive_axes(
|
|
|
1043
992
|
sys.stdout.write(f" {dim('Enter')} to apply · {dim('ESC')} to cancel\n")
|
|
1044
993
|
sys.stdout.flush()
|
|
1045
994
|
|
|
1046
|
-
|
|
1047
|
-
if not
|
|
1048
|
-
continue
|
|
1049
|
-
|
|
1050
|
-
raw = os.read(fd, 64)
|
|
1051
|
-
if not raw:
|
|
995
|
+
raw_bytes = read_burst(0.1)
|
|
996
|
+
if not raw_bytes:
|
|
1052
997
|
continue
|
|
1053
|
-
text =
|
|
998
|
+
text = raw_bytes.decode("ascii", errors="replace")
|
|
1054
999
|
i = 0
|
|
1055
1000
|
applied = False
|
|
1056
1001
|
while i < len(text):
|
|
@@ -1077,7 +1022,7 @@ def _submenu_freedrive_axes(
|
|
|
1077
1022
|
if applied:
|
|
1078
1023
|
break
|
|
1079
1024
|
finally:
|
|
1080
|
-
|
|
1025
|
+
raw.disable()
|
|
1081
1026
|
|
|
1082
1027
|
if sum(axes) == 0:
|
|
1083
1028
|
messages.append("Cancelled — at least 1 axis must be active")
|
|
@@ -1203,11 +1148,11 @@ def _teach_pendant(
|
|
|
1203
1148
|
print("Error: stdin is not a terminal. Run from an interactive shell.")
|
|
1204
1149
|
sys.exit(1)
|
|
1205
1150
|
|
|
1206
|
-
old_settings = termios.tcgetattr(sys.stdin)
|
|
1207
|
-
|
|
1208
1151
|
# SIGINT handler: restore terminal and exit when Ctrl+C pressed.
|
|
1209
|
-
#
|
|
1210
|
-
#
|
|
1152
|
+
# On Unix, raw mode keeps ISIG enabled, so Ctrl+C generates SIGINT.
|
|
1153
|
+
# On Windows, Ctrl+C arrives as a raw 0x03 byte instead (handled by
|
|
1154
|
+
# the main loop's exit key path). os._exit() works even when blocked
|
|
1155
|
+
# in C library code.
|
|
1211
1156
|
def _sigint_handler(signum: int, frame: object) -> None:
|
|
1212
1157
|
try:
|
|
1213
1158
|
if state["freedrive"]:
|
|
@@ -1224,7 +1169,7 @@ def _teach_pendant(
|
|
|
1224
1169
|
except Exception:
|
|
1225
1170
|
pass
|
|
1226
1171
|
try:
|
|
1227
|
-
_restore_terminal(
|
|
1172
|
+
_restore_terminal()
|
|
1228
1173
|
except Exception:
|
|
1229
1174
|
pass
|
|
1230
1175
|
sys.stderr.write("\nInterrupted.\n")
|
|
@@ -1235,7 +1180,13 @@ def _teach_pendant(
|
|
|
1235
1180
|
# Connection watchdog: detects faults and interrupts blocking calls.
|
|
1236
1181
|
monitor = ConnectionMonitor(robot)
|
|
1237
1182
|
monitor.start()
|
|
1238
|
-
|
|
1183
|
+
# SIGALRM does not exist on Windows: the monitor sets its
|
|
1184
|
+
# fault_detected flag instead, and the input-wait loops below check
|
|
1185
|
+
# it every 50ms. Faults during long blocking RTDE calls surface
|
|
1186
|
+
# when the call returns rather than being interrupted immediately.
|
|
1187
|
+
_old_sigalrm = None
|
|
1188
|
+
if hasattr(signal, "SIGALRM"):
|
|
1189
|
+
_old_sigalrm = signal.signal(signal.SIGALRM, monitor.alarm_handler)
|
|
1239
1190
|
global _cli_monitor
|
|
1240
1191
|
_cli_monitor = monitor # used by _read_input and _filter_select_points
|
|
1241
1192
|
|
|
@@ -1254,8 +1205,7 @@ def _teach_pendant(
|
|
|
1254
1205
|
|
|
1255
1206
|
# --- Wait for a key ---
|
|
1256
1207
|
while True:
|
|
1257
|
-
|
|
1258
|
-
if ready:
|
|
1208
|
+
if wait_input(0.05):
|
|
1259
1209
|
break
|
|
1260
1210
|
|
|
1261
1211
|
# Periodic refresh while in freedrive — robot may be moving by hand
|
|
@@ -1278,17 +1228,10 @@ def _teach_pendant(
|
|
|
1278
1228
|
# behind — when the user switches from one movement key to
|
|
1279
1229
|
# another, the last buffered copy of the old key fires one final
|
|
1280
1230
|
# move in the wrong direction before the new key is read.
|
|
1281
|
-
# Fix:
|
|
1282
|
-
#
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
ready, _, _ = select.select([sys.stdin], [], [], 0.0)
|
|
1286
|
-
if not ready:
|
|
1287
|
-
break
|
|
1288
|
-
ch = sys.stdin.read(1).lower()
|
|
1289
|
-
if not ch:
|
|
1290
|
-
break
|
|
1291
|
-
key = ch
|
|
1231
|
+
# Fix: drain all immediately available bytes
|
|
1232
|
+
# (non-blocking) and use the last one.
|
|
1233
|
+
key_text = read_burst(0.0).decode("ascii", errors="replace")
|
|
1234
|
+
key = key_text[-1].lower() if key_text else ""
|
|
1292
1235
|
if not key:
|
|
1293
1236
|
continue
|
|
1294
1237
|
|
|
@@ -1550,14 +1493,15 @@ def _teach_pendant(
|
|
|
1550
1493
|
finally:
|
|
1551
1494
|
# Stop monitor and restore signal handler
|
|
1552
1495
|
monitor.stop()
|
|
1553
|
-
|
|
1496
|
+
if _old_sigalrm is not None:
|
|
1497
|
+
signal.signal(signal.SIGALRM, _old_sigalrm)
|
|
1554
1498
|
|
|
1555
1499
|
finally:
|
|
1556
1500
|
_urkit_logger.removeHandler(_log_handler)
|
|
1557
1501
|
_urkit_logger.propagate = True
|
|
1558
1502
|
# Restore terminal
|
|
1559
1503
|
try:
|
|
1560
|
-
_restore_terminal(
|
|
1504
|
+
_restore_terminal()
|
|
1561
1505
|
except Exception:
|
|
1562
1506
|
pass
|
|
1563
1507
|
print("\n Exiting teach pendant.")
|
|
@@ -1570,6 +1514,8 @@ def teach_command(args) -> None:
|
|
|
1570
1514
|
args: Parsed arguments from argparse (with teach subcommand attributes).
|
|
1571
1515
|
"""
|
|
1572
1516
|
|
|
1517
|
+
warn_if_windows()
|
|
1518
|
+
|
|
1573
1519
|
# Configure logging
|
|
1574
1520
|
if args.verbose:
|
|
1575
1521
|
logging.basicConfig(
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Cross-platform raw terminal input for the interactive CLIs.
|
|
2
|
+
|
|
3
|
+
The CLIs need three things from the platform:
|
|
4
|
+
|
|
5
|
+
1. Raw mode on stdin (no line buffering, no echo) with a way to
|
|
6
|
+
restore the original settings, even on error paths.
|
|
7
|
+
2. Non-blocking input reads: "is there input?" and "read everything
|
|
8
|
+
currently buffered" (a burst, e.g. the 3-byte arrow-key sequence).
|
|
9
|
+
3. On Windows only: ANSI escape processing on stdout so colors and
|
|
10
|
+
cursor codes render.
|
|
11
|
+
|
|
12
|
+
Unix backend: termios + tty + select, exactly the syscalls the CLIs
|
|
13
|
+
used to issue directly.
|
|
14
|
+
|
|
15
|
+
Windows backend: msvcrt with console input mode changed via kernel32.
|
|
16
|
+
The console does not emit ANSI sequences for special keys, so the
|
|
17
|
+
backend translates extended keycodes (prefix byte 0xE0/0x00 + scan
|
|
18
|
+
code) into the same byte sequences a Unix terminal sends. This keeps
|
|
19
|
+
the CLI key-parsing code identical on both platforms.
|
|
20
|
+
|
|
21
|
+
Windows limitation: with ENABLE_PROCESSED_INPUT disabled, Ctrl+C is
|
|
22
|
+
delivered as a raw 0x03 byte instead of raising KeyboardInterrupt.
|
|
23
|
+
The CLIs already handle 0x03 as an exit key, and it matters that
|
|
24
|
+
Ctrl+C is a byte rather than an exception here: an exception would
|
|
25
|
+
bypass terminal restore.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import ctypes
|
|
31
|
+
import os
|
|
32
|
+
import sys
|
|
33
|
+
import time
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
if sys.platform == "win32":
|
|
37
|
+
import msvcrt
|
|
38
|
+
else:
|
|
39
|
+
import select
|
|
40
|
+
import termios
|
|
41
|
+
import tty
|
|
42
|
+
|
|
43
|
+
if sys.platform == "win32":
|
|
44
|
+
# CDLL is used instead of WinDLL because ctypes.WinDLL is only
|
|
45
|
+
# exposed in the Windows type stubs; on 64-bit Windows both use
|
|
46
|
+
# the same calling convention.
|
|
47
|
+
_kernel32 = ctypes.CDLL("kernel32", use_last_error=True)
|
|
48
|
+
_STD_INPUT_HANDLE = -10
|
|
49
|
+
_STD_OUTPUT_HANDLE = -11
|
|
50
|
+
_ENABLE_PROCESSED_INPUT = 0x0001
|
|
51
|
+
_ENABLE_LINE_INPUT = 0x0002
|
|
52
|
+
_ENABLE_ECHO_INPUT = 0x0004
|
|
53
|
+
_ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
|
54
|
+
|
|
55
|
+
# msvcrt extended-key scan codes (after the 0xE0/0x00 prefix byte)
|
|
56
|
+
# mapped to the ANSI CSI sequences a Unix terminal emits for the
|
|
57
|
+
# same key.
|
|
58
|
+
_EXTENDED_KEYS: dict[int, str] = {
|
|
59
|
+
0x48: "\x1b[A", # Up
|
|
60
|
+
0x50: "\x1b[B", # Down
|
|
61
|
+
0x4D: "\x1b[C", # Right
|
|
62
|
+
0x4B: "\x1b[D", # Left
|
|
63
|
+
0x47: "\x1b[H", # Home
|
|
64
|
+
0x4F: "\x1b[F", # End
|
|
65
|
+
0x49: "\x1b[5~", # Page Up
|
|
66
|
+
0x51: "\x1b[6~", # Page Down
|
|
67
|
+
0x52: "\x1b[2~", # Insert
|
|
68
|
+
0x53: "\x1b[3~", # Delete
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def _console_mode(handle: int) -> int:
|
|
72
|
+
mode = ctypes.c_uint32()
|
|
73
|
+
if not _kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
|
|
74
|
+
raise OSError(
|
|
75
|
+
"GetConsoleMode failed (stdin is not a console). "
|
|
76
|
+
"Run from an interactive shell."
|
|
77
|
+
)
|
|
78
|
+
return mode.value
|
|
79
|
+
|
|
80
|
+
def _set_console_mode(handle: int, mode: int) -> None:
|
|
81
|
+
if not _kernel32.SetConsoleMode(handle, mode):
|
|
82
|
+
raise OSError("SetConsoleMode failed")
|
|
83
|
+
|
|
84
|
+
def _enable_ansi_output() -> None:
|
|
85
|
+
"""Enable ANSI escape processing on stdout.
|
|
86
|
+
|
|
87
|
+
No-op on Windows Terminal and recent conhost; needed on
|
|
88
|
+
older conhost versions. Best effort: some non-console
|
|
89
|
+
stdout handles (redirected output) reject it.
|
|
90
|
+
"""
|
|
91
|
+
try:
|
|
92
|
+
handle = _kernel32.GetStdHandle(_STD_OUTPUT_HANDLE)
|
|
93
|
+
mode = _console_mode(handle)
|
|
94
|
+
_set_console_mode(
|
|
95
|
+
handle, mode | _ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
|
96
|
+
)
|
|
97
|
+
except OSError:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def warn_if_windows() -> None:
|
|
102
|
+
"""Print a notice when the interactive CLI runs on Windows.
|
|
103
|
+
|
|
104
|
+
Call at the top of interactive CLI commands. No-op on other
|
|
105
|
+
platforms. Windows CLI support is not fully tested.
|
|
106
|
+
"""
|
|
107
|
+
if sys.platform == "win32":
|
|
108
|
+
print(
|
|
109
|
+
"Warning: Windows CLI support is not fully tested and not "
|
|
110
|
+
"officially supported. If you hit input or console problems, "
|
|
111
|
+
"use Linux, macOS, or WSL2."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class RawTerminal:
|
|
116
|
+
"""Raw (cbreak, no-echo) mode for stdin.
|
|
117
|
+
|
|
118
|
+
Usage:
|
|
119
|
+
with RawTerminal():
|
|
120
|
+
...
|
|
121
|
+
or manually, where restore must also happen on explicit error
|
|
122
|
+
paths:
|
|
123
|
+
raw = RawTerminal()
|
|
124
|
+
raw.enable()
|
|
125
|
+
...
|
|
126
|
+
raw.disable() # idempotent, safe to call multiple times
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(self) -> None:
|
|
130
|
+
self._enabled = False
|
|
131
|
+
self._old_settings: list[Any] | None = None
|
|
132
|
+
self._old_mode: int | None = None
|
|
133
|
+
self._stdin_handle: int | None = None
|
|
134
|
+
|
|
135
|
+
def enable(self) -> None:
|
|
136
|
+
"""Enter raw mode. Idempotent."""
|
|
137
|
+
if self._enabled:
|
|
138
|
+
return
|
|
139
|
+
if sys.platform == "win32":
|
|
140
|
+
self._stdin_handle = _kernel32.GetStdHandle(_STD_INPUT_HANDLE)
|
|
141
|
+
self._old_mode = _console_mode(self._stdin_handle)
|
|
142
|
+
# Disable processed input, line input, and echo. Ctrl+C
|
|
143
|
+
# arrives as raw byte 0x03 (handled by the CLIs) instead
|
|
144
|
+
# of KeyboardInterrupt, which would skip terminal restore.
|
|
145
|
+
_set_console_mode(
|
|
146
|
+
self._stdin_handle,
|
|
147
|
+
self._old_mode
|
|
148
|
+
& ~(
|
|
149
|
+
_ENABLE_PROCESSED_INPUT
|
|
150
|
+
| _ENABLE_LINE_INPUT
|
|
151
|
+
| _ENABLE_ECHO_INPUT
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
_enable_ansi_output()
|
|
155
|
+
else:
|
|
156
|
+
self._old_settings = termios.tcgetattr(sys.stdin)
|
|
157
|
+
tty.setcbreak(sys.stdin.fileno())
|
|
158
|
+
self._enabled = True
|
|
159
|
+
|
|
160
|
+
def disable(self) -> None:
|
|
161
|
+
"""Restore the original terminal settings. Idempotent."""
|
|
162
|
+
if not self._enabled:
|
|
163
|
+
return
|
|
164
|
+
if sys.platform == "win32":
|
|
165
|
+
if self._stdin_handle is None or self._old_mode is None:
|
|
166
|
+
raise RuntimeError("RawTerminal disabled before enable()")
|
|
167
|
+
_set_console_mode(self._stdin_handle, self._old_mode)
|
|
168
|
+
else:
|
|
169
|
+
if self._old_settings is None:
|
|
170
|
+
raise RuntimeError("RawTerminal disabled before enable()")
|
|
171
|
+
termios.tcsetattr(
|
|
172
|
+
sys.stdin, termios.TCSADRAIN, self._old_settings
|
|
173
|
+
)
|
|
174
|
+
self._enabled = False
|
|
175
|
+
|
|
176
|
+
def __enter__(self) -> "RawTerminal":
|
|
177
|
+
self.enable()
|
|
178
|
+
return self
|
|
179
|
+
|
|
180
|
+
def __exit__(self, *exc: object) -> None:
|
|
181
|
+
self.disable()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _win_read_pending() -> bytes:
|
|
185
|
+
"""Read all pending console input, translating extended keys to
|
|
186
|
+
the ANSI CSI sequences a Unix terminal would have emitted."""
|
|
187
|
+
if sys.platform == "win32":
|
|
188
|
+
out = bytearray()
|
|
189
|
+
while msvcrt.kbhit():
|
|
190
|
+
byte = msvcrt.getch()
|
|
191
|
+
if byte in (b"\xe0", b"\x00"):
|
|
192
|
+
code = msvcrt.getch()
|
|
193
|
+
seq = _EXTENDED_KEYS.get(code)
|
|
194
|
+
if seq is None:
|
|
195
|
+
# Unrecognized function key (F1-F12 etc.): drop it
|
|
196
|
+
continue
|
|
197
|
+
out += seq.encode("ascii")
|
|
198
|
+
else:
|
|
199
|
+
out += byte
|
|
200
|
+
return bytes(out)
|
|
201
|
+
raise RuntimeError("_win_read_pending is Windows-only")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def wait_input(timeout: float) -> bool:
|
|
205
|
+
"""Return True if any stdin input is available within timeout."""
|
|
206
|
+
if sys.platform == "win32":
|
|
207
|
+
deadline = time.monotonic() + timeout
|
|
208
|
+
while not msvcrt.kbhit():
|
|
209
|
+
if time.monotonic() >= deadline:
|
|
210
|
+
return False
|
|
211
|
+
time.sleep(0.005)
|
|
212
|
+
return True
|
|
213
|
+
ready, _, _ = select.select([sys.stdin], [], [], timeout)
|
|
214
|
+
return bool(ready)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def read_burst(timeout: float) -> bytes:
|
|
218
|
+
"""Wait up to timeout for input, then return everything currently
|
|
219
|
+
buffered as one burst (e.g. the full \\x1b[A arrow-key sequence).
|
|
220
|
+
|
|
221
|
+
Returns b"" if no input arrives within timeout.
|
|
222
|
+
|
|
223
|
+
On Unix this is select() + a single os.read(), the same burst
|
|
224
|
+
semantics as reading once after a select() notification. On
|
|
225
|
+
Windows every pending key event is consumed.
|
|
226
|
+
"""
|
|
227
|
+
if sys.platform == "win32":
|
|
228
|
+
if not wait_input(timeout):
|
|
229
|
+
return b""
|
|
230
|
+
return _win_read_pending()
|
|
231
|
+
if not wait_input(timeout):
|
|
232
|
+
return b""
|
|
233
|
+
return os.read(sys.stdin.fileno(), 64)
|
|
@@ -14,11 +14,14 @@ from urkit.geometry import MoveFrame, transform_pose_delta
|
|
|
14
14
|
|
|
15
15
|
@dataclass(frozen=True, slots=True)
|
|
16
16
|
class Point:
|
|
17
|
-
"""A robot waypoint stored as a TCP pose.
|
|
17
|
+
"""A robot waypoint stored as a TCP pose in the base frame.
|
|
18
18
|
|
|
19
|
-
Stores the TCP pose
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
Stores the TCP pose from ``getActualTCPPose()`` as an absolute
|
|
20
|
+
position in the robot base frame. When passed to ``moveL()`` or
|
|
21
|
+
``moveJ()``, the controller moves whatever TCP is currently active
|
|
22
|
+
to that base-frame position. This means saved points remain valid
|
|
23
|
+
after TCP changes: the tool tip arrives at the same physical
|
|
24
|
+
location regardless of which tool is mounted.
|
|
22
25
|
|
|
23
26
|
Returned by Points attribute/subscript access. Pass directly
|
|
24
27
|
to URRobot.move_to(). Points loaded from the database carry a
|
|
@@ -26,7 +29,7 @@ class Point:
|
|
|
26
29
|
|
|
27
30
|
Fields:
|
|
28
31
|
name: Point name (key in the database, empty string for ad-hoc points).
|
|
29
|
-
pose: TCP pose [x, y, z, rx, ry, rz] in meters/radians.
|
|
32
|
+
pose: TCP pose [x, y, z, rx, ry, rz] in base frame, meters/radians.
|
|
30
33
|
"""
|
|
31
34
|
|
|
32
35
|
name: str
|
|
@@ -1891,19 +1891,20 @@ class URRobot:
|
|
|
1891
1891
|
"""Save a named point.
|
|
1892
1892
|
|
|
1893
1893
|
When *pose* is omitted, saves the current TCP pose from
|
|
1894
|
-
getActualTCPPose()
|
|
1894
|
+
``getActualTCPPose()``. When *pose* is provided, saves that
|
|
1895
1895
|
arbitrary pose directly without moving the robot.
|
|
1896
1896
|
|
|
1897
|
-
The
|
|
1898
|
-
frame
|
|
1899
|
-
|
|
1897
|
+
The pose is stored as an absolute position in the robot base
|
|
1898
|
+
frame. At playback, ``moveL()`` moves whatever TCP is currently
|
|
1899
|
+
active to that base-frame position, so saved points remain
|
|
1900
|
+
valid after TCP changes.
|
|
1900
1901
|
|
|
1901
1902
|
Overwrites if a point with the same name already exists.
|
|
1902
1903
|
|
|
1903
1904
|
Args:
|
|
1904
1905
|
name: Name for the saved point.
|
|
1905
|
-
pose: TCP pose [x, y, z, rx, ry, rz]
|
|
1906
|
-
robot position.
|
|
1906
|
+
pose: TCP pose [x, y, z, rx, ry, rz] in base frame.
|
|
1907
|
+
Defaults to current robot position.
|
|
1907
1908
|
|
|
1908
1909
|
Returns:
|
|
1909
1910
|
The saved Point object.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: urkit
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Universal Robots e-Series control toolkit built on ur_rtde
|
|
5
5
|
Author: URKit Contributors
|
|
6
6
|
License: MIT
|
|
@@ -94,6 +94,11 @@ The `-U` (upgrade) flag ensures you always get the latest version. This project
|
|
|
94
94
|
|
|
95
95
|
Requires Python 3.8+ and a Universal Robots e-Series (UR3e to UR30).
|
|
96
96
|
|
|
97
|
+
### Platform Support
|
|
98
|
+
|
|
99
|
+
- **Linux, macOS**: fully supported
|
|
100
|
+
- **Windows**: the Python library works on Windows. The interactive CLIs (`urkit teach`, `urkit points`) run on Windows but are **not fully tested and not officially supported**. A warning is printed at startup. If you hit input or console problems, use Linux, macOS, or WSL2
|
|
101
|
+
|
|
97
102
|
### Robot Setup (one-time)
|
|
98
103
|
|
|
99
104
|
1. **Network**: Connect your PC to the robot via Ethernet cable (direct) or a switch.
|
|
@@ -663,7 +668,7 @@ Default is `None` (controller handles IK natively). Set it globally when most of
|
|
|
663
668
|
|
|
664
669
|
#### Point Management
|
|
665
670
|
|
|
666
|
-
Points are stored in the
|
|
671
|
+
Points are stored as absolute TCP poses in the robot base frame. When you move to a point, the controller moves whatever TCP is currently active to that base-frame pose, so the tool tip lands at the same physical location. Change the TCP offset (or swap grippers) and your saved points stay valid.
|
|
667
672
|
|
|
668
673
|
```python
|
|
669
674
|
robot.save_point("here")
|
|
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
|