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.
Files changed (42) hide show
  1. {urkit-0.4.4 → urkit-0.4.5}/PKG-INFO +1 -1
  2. {urkit-0.4.4 → urkit-0.4.5}/pyproject.toml +1 -1
  3. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/__init__.py +1 -1
  4. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/cli/teach.py +89 -40
  5. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/cli/terminal.py +70 -1
  6. {urkit-0.4.4 → urkit-0.4.5}/src/urkit.egg-info/PKG-INFO +1 -1
  7. {urkit-0.4.4 → urkit-0.4.5}/README.md +0 -0
  8. {urkit-0.4.4 → urkit-0.4.5}/setup.cfg +0 -0
  9. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/__main__.py +0 -0
  10. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/cli/__init__.py +0 -0
  11. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/cli/colors.py +0 -0
  12. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/cli/connection_monitor.py +0 -0
  13. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/cli/init.py +0 -0
  14. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/cli/points.py +0 -0
  15. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/config.py +0 -0
  16. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/connection.py +0 -0
  17. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/exceptions.py +0 -0
  18. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/geometry.py +0 -0
  19. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/gripper/__init__.py +0 -0
  20. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/gripper/base.py +0 -0
  21. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/gripper/digital.py +0 -0
  22. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/gripper/presets.py +0 -0
  23. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/gripper/robotiq.py +0 -0
  24. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/gripper/robotiq_preamble.py +0 -0
  25. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/io.py +0 -0
  26. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/motion.py +0 -0
  27. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/points.py +0 -0
  28. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/robot.py +0 -0
  29. {urkit-0.4.4 → urkit-0.4.5}/src/urkit/telemetry.py +0 -0
  30. {urkit-0.4.4 → urkit-0.4.5}/src/urkit.egg-info/SOURCES.txt +0 -0
  31. {urkit-0.4.4 → urkit-0.4.5}/src/urkit.egg-info/dependency_links.txt +0 -0
  32. {urkit-0.4.4 → urkit-0.4.5}/src/urkit.egg-info/entry_points.txt +0 -0
  33. {urkit-0.4.4 → urkit-0.4.5}/src/urkit.egg-info/requires.txt +0 -0
  34. {urkit-0.4.4 → urkit-0.4.5}/src/urkit.egg-info/top_level.txt +0 -0
  35. {urkit-0.4.4 → urkit-0.4.5}/tests/test_exceptions.py +0 -0
  36. {urkit-0.4.4 → urkit-0.4.5}/tests/test_geometry.py +0 -0
  37. {urkit-0.4.4 → urkit-0.4.5}/tests/test_gripper.py +0 -0
  38. {urkit-0.4.4 → urkit-0.4.5}/tests/test_gripper_factory.py +0 -0
  39. {urkit-0.4.4 → urkit-0.4.5}/tests/test_gripper_presets.py +0 -0
  40. {urkit-0.4.4 → urkit-0.4.5}/tests/test_move_sequence.py +0 -0
  41. {urkit-0.4.4 → urkit-0.4.5}/tests/test_points.py +0 -0
  42. {urkit-0.4.4 → urkit-0.4.5}/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.4
3
+ Version: 0.4.5
4
4
  Summary: Universal Robots e-Series control toolkit built on ur_rtde
5
5
  Author: URKit Contributors
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "urkit"
7
- version = "0.4.4"
7
+ version = "0.4.5"
8
8
  description = "Universal Robots e-Series control toolkit built on ur_rtde"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -24,7 +24,7 @@ Quick start::
24
24
 
25
25
  from __future__ import annotations
26
26
 
27
- __version__ = "0.4.4"
27
+ __version__ = "0.4.5"
28
28
 
29
29
 
30
30
  from urkit.exceptions import (
@@ -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
- raw_bytes = read_burst(0.1)
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
- if wait_input(0.05):
743
- key = read_burst(0.0).decode("ascii", errors="replace")
744
- if " " in key: # Space to cancel
745
- cancelled = True
746
- break
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
- raw_bytes = read_burst(0.1)
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
- raw_bytes = read_burst(0.1)
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
- # SIGINT handler: restore terminal and exit when Ctrl+C pressed.
1187
- # On Unix, raw mode keeps ISIG enabled, so Ctrl+C generates SIGINT.
1188
- # On Windows, Ctrl+C arrives as a raw 0x03 byte instead (handled by
1189
- # the main loop's exit key path). os._exit() works even when blocked
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 _sigint_handler(signum: int, frame: object) -> None:
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("\nInterrupted.\n")
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
- while True:
1243
- if wait_input(0.05):
1244
- break
1245
-
1246
- # Periodic refresh while in freedrive — robot may be moving by hand
1247
- if state["freedrive"]:
1248
- now = time.monotonic()
1249
- if now - last_refresh >= _FREEDRIVE_REFRESH_S:
1250
- last_refresh = now
1251
- _draw_screen(robot, state, messages, expert_mode)
1252
-
1253
- # Check for fault detected by watchdog thread
1254
- if monitor.fault_detected:
1255
- raise URKitConnectionError(
1256
- f"Robot fault detected: {monitor._reason or 'RTDE connection lost'}. "
1257
- "RTDE connection lost."
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
- # --- Read key, draining buffered repeats ---
1261
- # When a key is held, the terminal auto-repeats it into the
1262
- # input buffer. Reading only one character leaves stale repeats
1263
- # behind — when the user switches from one movement key to
1264
- # another, the last buffered copy of the old key fires one final
1265
- # move in the wrong direction before the new key is read.
1266
- # Fix: drain all immediately available bytes
1267
- # (non-blocking) and use the last one.
1268
- key_text = read_burst(0.0).decode("ascii", errors="replace")
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: urkit
3
- Version: 0.4.4
3
+ Version: 0.4.5
4
4
  Summary: Universal Robots e-Series control toolkit built on ur_rtde
5
5
  Author: URKit Contributors
6
6
  License: MIT
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