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 +99 -0
- urkit/__main__.py +116 -0
- urkit/cli/__init__.py +6 -0
- urkit/cli/colors.py +99 -0
- urkit/cli/connection_monitor.py +154 -0
- urkit/cli/points.py +293 -0
- urkit/cli/teach.py +1181 -0
- urkit/config.py +78 -0
- urkit/connection.py +580 -0
- urkit/exceptions.py +51 -0
- urkit/geometry.py +413 -0
- urkit/gripper/__init__.py +36 -0
- urkit/gripper/base.py +116 -0
- urkit/gripper/digital.py +140 -0
- urkit/gripper/presets.py +113 -0
- urkit/gripper/robotiq.py +322 -0
- urkit/gripper/robotiq_preamble.py +1356 -0
- urkit/io.py +311 -0
- urkit/motion.py +504 -0
- urkit/points.py +277 -0
- urkit/robot.py +1510 -0
- urkit/telemetry.py +177 -0
- urkit-0.1.0.dist-info/METADATA +612 -0
- urkit-0.1.0.dist-info/RECORD +27 -0
- urkit-0.1.0.dist-info/WHEEL +5 -0
- urkit-0.1.0.dist-info/entry_points.txt +2 -0
- urkit-0.1.0.dist-info/top_level.txt +1 -0
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
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)
|