urkit 0.4.4__tar.gz → 0.4.5__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.5}/PKG-INFO +1 -1
- {urkit-0.4.4 → urkit-0.4.5}/pyproject.toml +1 -1
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/__init__.py +1 -1
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/cli/teach.py +89 -40
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/cli/terminal.py +70 -1
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit.egg-info/PKG-INFO +1 -1
- {urkit-0.4.4 → urkit-0.4.5}/README.md +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/setup.cfg +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/__main__.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/cli/__init__.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/cli/colors.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/cli/connection_monitor.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/cli/init.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/cli/points.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/config.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/connection.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/exceptions.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/geometry.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/gripper/__init__.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/gripper/base.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/gripper/digital.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/gripper/presets.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/gripper/robotiq.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/gripper/robotiq_preamble.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/io.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/motion.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/points.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/robot.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit/telemetry.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit.egg-info/SOURCES.txt +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit.egg-info/dependency_links.txt +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit.egg-info/entry_points.txt +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit.egg-info/requires.txt +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/src/urkit.egg-info/top_level.txt +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/tests/test_exceptions.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/tests/test_geometry.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/tests/test_gripper.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/tests/test_gripper_factory.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/tests/test_gripper_presets.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/tests/test_move_sequence.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/tests/test_points.py +0 -0
- {urkit-0.4.4 → urkit-0.4.5}/tests/test_robot_integration.py +0 -0
|
@@ -26,7 +26,9 @@ import yaml
|
|
|
26
26
|
from urkit.cli.colors import blue, cyan, dim, green, red, yellow
|
|
27
27
|
from urkit.cli.terminal import (
|
|
28
28
|
RawTerminal,
|
|
29
|
+
input_active,
|
|
29
30
|
read_burst,
|
|
31
|
+
start_interrupt_watcher,
|
|
30
32
|
wait_input,
|
|
31
33
|
warn_if_windows,
|
|
32
34
|
)
|
|
@@ -475,6 +477,7 @@ def _read_input(prompt: str, max_len: int = 30) -> str | None:
|
|
|
475
477
|
sys.stdout.flush()
|
|
476
478
|
|
|
477
479
|
name = ""
|
|
480
|
+
input_active.set()
|
|
478
481
|
try:
|
|
479
482
|
while True:
|
|
480
483
|
if not wait_input(0.1):
|
|
@@ -511,6 +514,7 @@ def _read_input(prompt: str, max_len: int = 30) -> str | None:
|
|
|
511
514
|
break
|
|
512
515
|
finally:
|
|
513
516
|
raw.disable()
|
|
517
|
+
input_active.clear()
|
|
514
518
|
|
|
515
519
|
sys.stdout.write("\r\033[K\n")
|
|
516
520
|
sys.stdout.flush()
|
|
@@ -624,7 +628,11 @@ def _filter_select_points(
|
|
|
624
628
|
|
|
625
629
|
# Wait for input, then read the full burst (e.g. the
|
|
626
630
|
# 3-byte \x1b[A arrow-key sequence) in one go.
|
|
627
|
-
|
|
631
|
+
input_active.set()
|
|
632
|
+
try:
|
|
633
|
+
raw_bytes = read_burst(0.1)
|
|
634
|
+
finally:
|
|
635
|
+
input_active.clear()
|
|
628
636
|
if not raw_bytes:
|
|
629
637
|
if _cli_monitor and _cli_monitor.fault_detected:
|
|
630
638
|
raise URKitConnectionError(
|
|
@@ -739,11 +747,15 @@ def _submenu_goto_point(
|
|
|
739
747
|
robot, name, mode_label, start_pose, target_pose, is_cartesian, max_progress
|
|
740
748
|
)
|
|
741
749
|
max_progress = pct
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
if
|
|
745
|
-
|
|
746
|
-
|
|
750
|
+
input_active.set()
|
|
751
|
+
try:
|
|
752
|
+
if wait_input(0.05):
|
|
753
|
+
key = read_burst(0.0).decode("ascii", errors="replace")
|
|
754
|
+
if " " in key: # Space to cancel
|
|
755
|
+
cancelled = True
|
|
756
|
+
break
|
|
757
|
+
finally:
|
|
758
|
+
input_active.clear()
|
|
747
759
|
|
|
748
760
|
# Clear moving screen
|
|
749
761
|
sys.stdout.write("\033[2J\033[1;1H")
|
|
@@ -903,7 +915,11 @@ def _submenu_orient_tcp(
|
|
|
903
915
|
sys.stdout.flush()
|
|
904
916
|
needs_redraw = False
|
|
905
917
|
|
|
906
|
-
|
|
918
|
+
input_active.set()
|
|
919
|
+
try:
|
|
920
|
+
raw_bytes = read_burst(0.1)
|
|
921
|
+
finally:
|
|
922
|
+
input_active.clear()
|
|
907
923
|
if not raw_bytes:
|
|
908
924
|
if _cli_monitor and _cli_monitor.fault_detected:
|
|
909
925
|
raise URKitConnectionError(
|
|
@@ -1022,7 +1038,11 @@ def _submenu_freedrive_axes(
|
|
|
1022
1038
|
sys.stdout.flush()
|
|
1023
1039
|
needs_redraw = False
|
|
1024
1040
|
|
|
1025
|
-
|
|
1041
|
+
input_active.set()
|
|
1042
|
+
try:
|
|
1043
|
+
raw_bytes = read_burst(0.1)
|
|
1044
|
+
finally:
|
|
1045
|
+
input_active.clear()
|
|
1026
1046
|
if not raw_bytes:
|
|
1027
1047
|
continue
|
|
1028
1048
|
text = raw_bytes.decode("ascii", errors="replace")
|
|
@@ -1183,12 +1203,12 @@ def _teach_pendant(
|
|
|
1183
1203
|
print("Error: stdin is not a terminal. Run from an interactive shell.")
|
|
1184
1204
|
sys.exit(1)
|
|
1185
1205
|
|
|
1186
|
-
#
|
|
1187
|
-
#
|
|
1188
|
-
#
|
|
1189
|
-
#
|
|
1206
|
+
# Best-effort robot stop and terminal restore, then exit.
|
|
1207
|
+
# Runs from the SIGINT handler (Unix Ctrl+C) and from the
|
|
1208
|
+
# interrupt watcher (Windows Ctrl+C / fault while the main thread
|
|
1209
|
+
# is blocked in a robot call). os._exit() works even when blocked
|
|
1190
1210
|
# in C library code.
|
|
1191
|
-
def
|
|
1211
|
+
def _interrupt_exit(reason: str) -> None:
|
|
1192
1212
|
try:
|
|
1193
1213
|
if state["freedrive"]:
|
|
1194
1214
|
robot.disable_freedrive()
|
|
@@ -1207,9 +1227,16 @@ def _teach_pendant(
|
|
|
1207
1227
|
_restore_terminal()
|
|
1208
1228
|
except Exception:
|
|
1209
1229
|
pass
|
|
1210
|
-
sys.stderr.write("\
|
|
1230
|
+
sys.stderr.write(f"\n{reason}\n")
|
|
1211
1231
|
os._exit(0)
|
|
1212
1232
|
|
|
1233
|
+
# SIGINT handler: restore terminal and exit when Ctrl+C pressed.
|
|
1234
|
+
# On Unix, raw mode keeps ISIG enabled, so Ctrl+C generates SIGINT.
|
|
1235
|
+
# On Windows, Ctrl+C arrives as a raw 0x03 byte instead (handled by
|
|
1236
|
+
# the main loop's exit key path and the interrupt watcher).
|
|
1237
|
+
def _sigint_handler(signum: int, frame: object) -> None:
|
|
1238
|
+
_interrupt_exit("Interrupted.")
|
|
1239
|
+
|
|
1213
1240
|
signal.signal(signal.SIGINT, _sigint_handler)
|
|
1214
1241
|
|
|
1215
1242
|
# Connection watchdog: detects faults and interrupts blocking calls.
|
|
@@ -1225,6 +1252,21 @@ def _teach_pendant(
|
|
|
1225
1252
|
global _cli_monitor
|
|
1226
1253
|
_cli_monitor = monitor # used by _read_input and _filter_select_points
|
|
1227
1254
|
|
|
1255
|
+
# Windows escape hatch: no SIGINT delivery and no SIGALRM, so while
|
|
1256
|
+
# the main thread is blocked in a long RTDE call (e.g. a move while
|
|
1257
|
+
# a key is held) neither Ctrl+C (a console byte) nor a detected
|
|
1258
|
+
# fault can reach it. The watcher fires the same cleanup and exit.
|
|
1259
|
+
def _watcher_fault_reason() -> str | None:
|
|
1260
|
+
if monitor.fault_detected:
|
|
1261
|
+
return (
|
|
1262
|
+
f"Robot fault detected: "
|
|
1263
|
+
f"{monitor._reason or 'RTDE connection lost'}. "
|
|
1264
|
+
"RTDE connection lost."
|
|
1265
|
+
)
|
|
1266
|
+
return None
|
|
1267
|
+
|
|
1268
|
+
start_interrupt_watcher(_interrupt_exit, _watcher_fault_reason)
|
|
1269
|
+
|
|
1228
1270
|
try:
|
|
1229
1271
|
try:
|
|
1230
1272
|
_configure_terminal()
|
|
@@ -1239,33 +1281,40 @@ def _teach_pendant(
|
|
|
1239
1281
|
command_handled = False
|
|
1240
1282
|
|
|
1241
1283
|
# --- Wait for a key ---
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1284
|
+
# input_active tells the Windows interrupt watcher to
|
|
1285
|
+
# stay out of the console queue while we read.
|
|
1286
|
+
input_active.set()
|
|
1287
|
+
try:
|
|
1288
|
+
while True:
|
|
1289
|
+
if wait_input(0.05):
|
|
1290
|
+
break
|
|
1291
|
+
|
|
1292
|
+
# Periodic refresh while in freedrive — robot may be moving by hand
|
|
1293
|
+
if state["freedrive"]:
|
|
1294
|
+
now = time.monotonic()
|
|
1295
|
+
if now - last_refresh >= _FREEDRIVE_REFRESH_S:
|
|
1296
|
+
last_refresh = now
|
|
1297
|
+
_draw_screen(robot, state, messages, expert_mode)
|
|
1298
|
+
|
|
1299
|
+
# Check for fault detected by watchdog thread
|
|
1300
|
+
if monitor.fault_detected:
|
|
1301
|
+
raise URKitConnectionError(
|
|
1302
|
+
f"Robot fault detected: {monitor._reason or 'RTDE connection lost'}. "
|
|
1303
|
+
"RTDE connection lost."
|
|
1304
|
+
)
|
|
1259
1305
|
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1306
|
+
# --- Read key, draining buffered repeats ---
|
|
1307
|
+
# When a key is held, the terminal auto-repeats it into the
|
|
1308
|
+
# input buffer. Reading only one character leaves stale
|
|
1309
|
+
# repeats behind — when the user switches from one
|
|
1310
|
+
# movement key to another, the last buffered copy of the
|
|
1311
|
+
# old key fires one final move in the wrong direction
|
|
1312
|
+
# before the new key is read.
|
|
1313
|
+
# Fix: drain all immediately available bytes
|
|
1314
|
+
# (non-blocking) and use the last one.
|
|
1315
|
+
key_text = read_burst(0.0).decode("ascii", errors="replace")
|
|
1316
|
+
finally:
|
|
1317
|
+
input_active.clear()
|
|
1269
1318
|
key = key_text[-1].lower() if key_text else ""
|
|
1270
1319
|
if not key:
|
|
1271
1320
|
continue
|
|
@@ -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,74 @@ 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
|
+
def start_interrupt_watcher(
|
|
139
|
+
on_fire: Callable[[str], None],
|
|
140
|
+
fault_reason: Callable[[], str | None],
|
|
141
|
+
) -> threading.Thread | None:
|
|
142
|
+
"""Start the Windows-only interrupt watcher. Returns None elsewhere.
|
|
143
|
+
|
|
144
|
+
On Windows, while the main thread is blocked in a long RTDE call
|
|
145
|
+
(not reading input) nothing can interrupt it: Ctrl+C is a raw
|
|
146
|
+
console byte that only the input loops read, and there is no
|
|
147
|
+
SIGALRM. This daemon thread covers that gap. It polls, only while
|
|
148
|
+
input_active is clear:
|
|
149
|
+
|
|
150
|
+
- fault_reason() for a connection/robot fault
|
|
151
|
+
- the console for a Ctrl+C (0x03) byte; any other byte is pushed
|
|
152
|
+
back with ungetch() so the main reader still sees it, and the
|
|
153
|
+
watcher backs off to avoid interfering with resumed input
|
|
154
|
+
|
|
155
|
+
When either fires it calls on_fire(reason) exactly once; the
|
|
156
|
+
caller should clean up (stop robot, restore terminal) and exit.
|
|
157
|
+
"""
|
|
158
|
+
if sys.platform != "win32":
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
# Only reachable on Windows (guard above), so this never runs on
|
|
162
|
+
# Linux. Local import because mypy prunes the top-level platform
|
|
163
|
+
# guard and cannot see the module inside this closure.
|
|
164
|
+
import msvcrt
|
|
165
|
+
|
|
166
|
+
def _run() -> None:
|
|
167
|
+
backoff = 0.0
|
|
168
|
+
while True:
|
|
169
|
+
if backoff > 0:
|
|
170
|
+
time.sleep(backoff)
|
|
171
|
+
backoff = 0.0
|
|
172
|
+
if input_active.is_set():
|
|
173
|
+
time.sleep(0.05)
|
|
174
|
+
continue
|
|
175
|
+
reason = fault_reason()
|
|
176
|
+
if reason is not None:
|
|
177
|
+
on_fire(reason)
|
|
178
|
+
return
|
|
179
|
+
if not msvcrt.kbhit():
|
|
180
|
+
time.sleep(0.05)
|
|
181
|
+
continue
|
|
182
|
+
byte = msvcrt.getch()
|
|
183
|
+
if byte == b"\x03":
|
|
184
|
+
on_fire("Interrupted.")
|
|
185
|
+
return
|
|
186
|
+
# Not Ctrl+C: give the byte back and back off so the main
|
|
187
|
+
# reader (resuming from a blocking call) processes it.
|
|
188
|
+
msvcrt.ungetch(byte[0])
|
|
189
|
+
backoff = 0.2
|
|
190
|
+
|
|
191
|
+
watcher = threading.Thread(
|
|
192
|
+
target=_run, daemon=True, name="interrupt-watcher"
|
|
193
|
+
)
|
|
194
|
+
watcher.start()
|
|
195
|
+
return watcher
|
|
196
|
+
|
|
197
|
+
|
|
129
198
|
def warn_if_windows() -> None:
|
|
130
199
|
"""Print a notice when the interactive CLI runs on Windows.
|
|
131
200
|
|
|
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
|
|
File without changes
|
|
File without changes
|