urkit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
urkit/__init__.py ADDED
@@ -0,0 +1,99 @@
1
+ """URKit — Universal Robots e-Series control toolkit.
2
+
3
+ A high-level Python package for Universal Robots e-Series (UR5e, UR10e,
4
+ UR16e) built on ``ur_rtde``. Provides connection validation, named point
5
+ management, motion commands, gripper abstraction, telemetry, geometry
6
+ helpers, and an interactive teach pendant CLI.
7
+
8
+ Quick start::
9
+
10
+ from urkit import URRobot, ROBOTIQ_HAND_E
11
+
12
+ robot = URRobot(
13
+ ip="192.168.1.100",
14
+ points="points.db",
15
+ gripper=ROBOTIQ_HAND_E,
16
+ )
17
+
18
+ robot.gripper.activate()
19
+ robot.move_to("pick")
20
+ robot.gripper.open()
21
+ robot.move_to("place")
22
+ robot.gripper.close()
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from urkit.config import load_config, resolve_config
28
+ from urkit.exceptions import (
29
+ URKitError,
30
+ URKitConnectionError,
31
+ MotionError,
32
+ GripperError,
33
+ PointError,
34
+ URKitIOError,
35
+ TelemetryError,
36
+ URKitRuntimeError,
37
+ RobotNotInRemoteModeError,
38
+ RtdeRegisterConflictError,
39
+ )
40
+ from urkit.geometry import (
41
+ MoveFrame,
42
+ orient_tcp_down,
43
+ quat_to_rotvec,
44
+ quat_to_rpy,
45
+ rpy_to_quat,
46
+ rotvec_to_quat,
47
+ )
48
+ from urkit.gripper import Gripper
49
+ from urkit.gripper.presets import (
50
+ DigitalGripperConfig,
51
+ GripperPreset,
52
+ PRESETS,
53
+ ROBOTIQ_2F_85,
54
+ ROBOTIQ_2F_140,
55
+ ROBOTIQ_HAND_E,
56
+ )
57
+ from urkit.motion import FreedriveMode
58
+ from urkit.robot import URRobot
59
+
60
+ __version__ = "0.0.1"
61
+
62
+ __all__ = [
63
+ # Version
64
+ "__version__",
65
+ # Config
66
+ "load_config",
67
+ "resolve_config",
68
+ # Core class
69
+ "URRobot",
70
+ # Gripper
71
+ "Gripper",
72
+ "GripperPreset",
73
+ "DigitalGripperConfig",
74
+ "PRESETS",
75
+ "ROBOTIQ_2F_85",
76
+ "ROBOTIQ_2F_140",
77
+ "ROBOTIQ_HAND_E",
78
+ # Freedrive mode
79
+ "FreedriveMode",
80
+ # Move frame
81
+ "MoveFrame",
82
+ # Geometry
83
+ "orient_tcp_down",
84
+ "quat_to_rotvec",
85
+ "quat_to_rpy",
86
+ "rpy_to_quat",
87
+ "rotvec_to_quat",
88
+ # Exceptions
89
+ "URKitError",
90
+ "URKitConnectionError",
91
+ "MotionError",
92
+ "GripperError",
93
+ "PointError",
94
+ "URKitIOError",
95
+ "TelemetryError",
96
+ "URKitRuntimeError",
97
+ "RobotNotInRemoteModeError",
98
+ "RtdeRegisterConflictError",
99
+ ]
urkit/__main__.py ADDED
@@ -0,0 +1,116 @@
1
+ """urkit CLI entry point.
2
+
3
+ Usage:
4
+ urkit teach 192.168.1.100
5
+ urkit teach 192.168.1.100 --gripper=hand-e
6
+ urkit points list
7
+ urkit points list --filter "pick*"
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import sys
14
+
15
+ from urkit.cli.teach import teach_command
16
+ from urkit.cli.points import points_command
17
+
18
+
19
+ def main() -> None:
20
+ """Main CLI dispatcher for urkit subcommands."""
21
+ parser = argparse.ArgumentParser(
22
+ prog="urkit",
23
+ description="URKit — Universal Robots e-Series control toolkit",
24
+ )
25
+
26
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
27
+
28
+ # teach subcommand
29
+ teach_parser = subparsers.add_parser(
30
+ "teach",
31
+ help="Launch interactive teach pendant",
32
+ )
33
+ teach_parser.add_argument(
34
+ "ip",
35
+ type=str,
36
+ nargs="?",
37
+ default=None,
38
+ help="Robot IP address (overrides config file)",
39
+ )
40
+ teach_parser.add_argument(
41
+ "--gripper",
42
+ type=str,
43
+ default=None,
44
+ choices=["2f-85", "2f-140", "hand-e", "digital"],
45
+ help="Gripper preset (overrides config file)",
46
+ )
47
+ teach_parser.add_argument(
48
+ "--gripper-pin",
49
+ type=int,
50
+ default=None,
51
+ help="Digital gripper output pin (overrides config file)",
52
+ )
53
+ teach_parser.add_argument(
54
+ "--gripper-force",
55
+ type=int,
56
+ default=None,
57
+ help="Robotiq gripper force 0-100 (overrides config file)",
58
+ )
59
+ teach_parser.add_argument(
60
+ "--gripper-speed",
61
+ type=int,
62
+ default=None,
63
+ help="Robotiq gripper speed 0-100 (overrides config file)",
64
+ )
65
+ teach_parser.add_argument(
66
+ "--gripper-close-on-high",
67
+ type=str,
68
+ default=None,
69
+ choices=["true", "false"],
70
+ help="Digital gripper polarity: true=HIGH closes, false=LOW closes",
71
+ )
72
+ teach_parser.add_argument(
73
+ "--points",
74
+ type=str,
75
+ default=None,
76
+ help="Path to points.db file (overrides config file)",
77
+ )
78
+ teach_parser.add_argument(
79
+ "-v", "--verbose",
80
+ action="store_true",
81
+ default=False,
82
+ help="Show verbose output (useful for debugging connection issues)",
83
+ )
84
+
85
+ # points subcommand
86
+ points_parser = subparsers.add_parser(
87
+ "points",
88
+ help="Browse saved points interactively",
89
+ )
90
+ points_parser.add_argument(
91
+ "points_path",
92
+ type=str,
93
+ nargs="?",
94
+ default=None,
95
+ help="Path to points.db file (defaults to points.db in config or current directory)",
96
+ )
97
+
98
+ args = parser.parse_args()
99
+
100
+ if not args.command:
101
+ parser.print_help()
102
+ sys.exit(1)
103
+
104
+ if args.command == "teach":
105
+ teach_command(args)
106
+ elif args.command == "points":
107
+ points_command(args)
108
+
109
+
110
+ def main_entry() -> None:
111
+ """Entry point for console script."""
112
+ main()
113
+
114
+
115
+ if __name__ == "__main__":
116
+ main_entry()
urkit/cli/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """CLI subcommand dispatch for urkit."""
2
+
3
+ from urkit.cli.teach import teach_command
4
+ from urkit.cli.points import points_command
5
+
6
+ __all__ = ["teach_command", "points_command"]
urkit/cli/colors.py ADDED
@@ -0,0 +1,99 @@
1
+ """Cross-platform terminal color helpers.
2
+
3
+ Uses colorama for Windows compatibility. Detects dark/light terminal
4
+ and adjusts colors for readability.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+
11
+ import colorama
12
+
13
+ colorama.init()
14
+
15
+ # ANSI codes (colorama makes these work on Windows)
16
+ RST = "\033[0m"
17
+ BOLD = "\033[1m"
18
+
19
+
20
+ def _is_dark_terminal() -> bool:
21
+ """Detect if the terminal is using a dark background."""
22
+ cfbg = os.environ.get("COLORFGBG", "")
23
+ if cfbg and ":" in cfbg:
24
+ try:
25
+ bg = int(cfbg.split(":")[1])
26
+ return bg in (0, 1, 2, 3, 4, 5, 6)
27
+ except (ValueError, IndexError):
28
+ pass
29
+ if os.environ.get("NO_COLOR"):
30
+ return False
31
+ if os.environ.get("TERM_PROGRAM") == "vscode":
32
+ theme = os.environ.get("VSCODE_THEME_COLORID", "")
33
+ return "dark" in theme.lower()
34
+ return True
35
+
36
+
37
+ # Two complete palettes
38
+ _DARK = {
39
+ "DIM": "\033[2m",
40
+ "GREEN": "\033[92m",
41
+ "YELLOW": "\033[93m",
42
+ "BLUE": "\033[94m",
43
+ "CYAN": "\033[96m",
44
+ "RED": "\033[91m",
45
+ "WHITE": "\033[97m",
46
+ }
47
+
48
+ _LIGHT = {
49
+ "DIM": "\033[2m",
50
+ "GREEN": "\033[32m",
51
+ "YELLOW": "\033[33m",
52
+ "BLUE": "\033[34m",
53
+ "CYAN": "\033[36m",
54
+ "RED": "\033[31m",
55
+ "WHITE": "\033[37m",
56
+ }
57
+
58
+ _PALETTE = _DARK if _is_dark_terminal() else _LIGHT
59
+
60
+ DIM = _PALETTE["DIM"]
61
+ GREEN = _PALETTE["GREEN"]
62
+ YELLOW = _PALETTE["YELLOW"]
63
+ BLUE = _PALETTE["BLUE"]
64
+ CYAN = _PALETTE["CYAN"]
65
+ RED = _PALETTE["RED"]
66
+ WHITE = _PALETTE["WHITE"]
67
+
68
+
69
+ def _wrap(text: str, *codes: str) -> str:
70
+ """Wrap text with ANSI codes and reset."""
71
+ return "".join(codes) + text + RST
72
+
73
+
74
+ def bold(text: str) -> str:
75
+ return _wrap(text, BOLD)
76
+
77
+
78
+ def green(text: str) -> str:
79
+ return _wrap(text, GREEN)
80
+
81
+
82
+ def yellow(text: str) -> str:
83
+ return _wrap(text, YELLOW)
84
+
85
+
86
+ def blue(text: str) -> str:
87
+ return _wrap(text, BLUE)
88
+
89
+
90
+ def cyan(text: str) -> str:
91
+ return _wrap(text, CYAN)
92
+
93
+
94
+ def red(text: str) -> str:
95
+ return _wrap(text, RED)
96
+
97
+
98
+ def dim(text: str) -> str:
99
+ return _wrap(text, DIM)
@@ -0,0 +1,154 @@
1
+ """Connection monitoring for the teach pendant CLI.
2
+
3
+ Runs a daemon thread that periodically checks the RTDE connection
4
+ and robot safety state. Sends SIGALRM to interrupt blocking RTDE
5
+ calls when a fault or connection drop is detected.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import os
12
+ import signal
13
+ import threading
14
+ from typing import TYPE_CHECKING
15
+
16
+ from urkit.exceptions import URKitConnectionError
17
+
18
+ if TYPE_CHECKING:
19
+ from urkit.robot import URRobot
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class ConnectionMonitor:
25
+ """Background thread that monitors RTDE connection and robot state.
26
+
27
+ Checks isConnected() on the RTDE interfaces and the robot's
28
+ protective/emergency stop state. When a fault is detected, sends
29
+ SIGALRM to the process to interrupt any blocking RTDE call.
30
+
31
+ Args:
32
+ robot: URRobot instance to monitor.
33
+ interval: Seconds between checks (default 0.5).
34
+ grace_period: Seconds to wait before the first check (default
35
+ 2.0). Prevents false triggers while the CLI is initializing.
36
+
37
+ Example:
38
+ >>> monitor = ConnectionMonitor(robot)
39
+ >>> monitor.start()
40
+ >>> signal.signal(signal.SIGALRM, monitor.alarm_handler)
41
+ >>> try:
42
+ ... # main loop with blocking RTDE calls
43
+ ... except URKitConnectionError as e:
44
+ ... print(f"Fault: {e}")
45
+ >>> monitor.stop()
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ robot: "URRobot",
51
+ interval: float = 0.5,
52
+ grace_period: float = 2.0,
53
+ ) -> None:
54
+ self._robot = robot
55
+ self._interval = interval
56
+ self._grace_period = grace_period
57
+ self._stop_event = threading.Event()
58
+ self._fault_event = threading.Event()
59
+ self._thread: threading.Thread | None = None
60
+ self._reason: str | None = None
61
+
62
+ def start(self) -> None:
63
+ """Start the monitoring thread."""
64
+ if self._thread is not None and self._thread.is_alive():
65
+ return
66
+ self._stop_event.clear()
67
+ self._fault_event.clear()
68
+ self._reason = None
69
+ self._thread = threading.Thread(
70
+ target=self._monitor_loop,
71
+ daemon=True,
72
+ name="connection-monitor",
73
+ )
74
+ self._thread.start()
75
+ logger.info("Connection monitor started (interval=%.1fs)", self._interval)
76
+
77
+ def stop(self) -> None:
78
+ """Stop the monitoring thread."""
79
+ self._stop_event.set()
80
+ if self._thread is not None:
81
+ self._thread.join(timeout=2.0)
82
+ self._thread = None
83
+ logger.info("Connection monitor stopped")
84
+
85
+ @property
86
+ def is_alive(self) -> bool:
87
+ """Return whether the monitoring thread is running."""
88
+ return self._thread is not None and self._thread.is_alive()
89
+
90
+ @property
91
+ def fault_detected(self) -> bool:
92
+ """Return True if a fault has been detected by the monitor."""
93
+ return self._fault_event.is_set()
94
+
95
+ def alarm_handler(self, signum: int, frame: object) -> None:
96
+ """Signal handler for SIGALRM. Raises URKitConnectionError.
97
+
98
+ Install this as the SIGALRM handler before entering any
99
+ blocking RTDE call:
100
+ signal.signal(signal.SIGALRM, monitor.alarm_handler)
101
+ """
102
+ reason = self._reason or "RTDE connection lost"
103
+ raise URKitConnectionError(
104
+ f"Robot fault detected: {reason}. RTDE connection lost."
105
+ )
106
+
107
+ def _monitor_loop(self) -> None:
108
+ """Main monitoring loop running in the daemon thread."""
109
+ # Grace period — wait before the first check so the CLI has time
110
+ # to draw the screen and set up terminal mode. Prevents SIGALRM
111
+ # from interrupting a blocking call before the UI is ready.
112
+ if self._grace_period > 0:
113
+ logger.info("Monitor grace period: %.1fs", self._grace_period)
114
+ if self._stop_event.wait(self._grace_period):
115
+ return
116
+
117
+ while not self._stop_event.wait(self._interval):
118
+ try:
119
+ # Check RTDE connection status
120
+ rtde_c = self._robot.rtde_control
121
+ if not rtde_c.isConnected():
122
+ self._trigger("RTDE control connection lost")
123
+ return
124
+
125
+ # Check for protective stop
126
+ if self._robot.is_protective_stopped():
127
+ self._trigger("Robot is in protective stop")
128
+ return
129
+
130
+ # Check for emergency stop
131
+ if self._robot.is_emergency_stopped():
132
+ self._trigger("Robot is in emergency stop")
133
+ return
134
+
135
+ except URKitConnectionError:
136
+ # Re-raise alarm-triggered errors
137
+ raise
138
+ except Exception as e:
139
+ # Any exception reading state suggests connection issues
140
+ logger.warning("Connection monitor error: %s", e)
141
+ self._trigger(f"Robot connection fault: {e}")
142
+ return
143
+
144
+ logger.info("Connection monitor loop exited")
145
+
146
+ def _trigger(self, reason: str) -> None:
147
+ """Record fault reason and send SIGALRM to interrupt blocking calls."""
148
+ self._reason = reason
149
+ self._fault_event.set()
150
+ logger.info("Fault detected: %s", reason)
151
+ try:
152
+ os.kill(os.getpid(), signal.SIGALRM)
153
+ except OSError as e:
154
+ logger.error("Failed to send SIGALRM: %s", e)