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/cli/points.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Points explorer — interactive terminal UI for browsing saved points.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
urkit points
|
|
5
|
+
Opens an interactive explorer with real-time search filtering.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import difflib
|
|
11
|
+
import math
|
|
12
|
+
import select
|
|
13
|
+
import sys
|
|
14
|
+
import termios
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
|
|
20
|
+
from urkit import load_config
|
|
21
|
+
from urkit.cli.colors import blue, cyan, dim, yellow
|
|
22
|
+
from urkit.points import Points
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def points_command(args) -> None:
|
|
26
|
+
"""Execute the points command.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
args: Parsed arguments from argparse (with points subcommand attributes).
|
|
30
|
+
"""
|
|
31
|
+
_explore_points(args)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _euclidean_distance(pose1: list, pose2: list) -> float:
|
|
35
|
+
"""Calculate Euclidean distance between two poses (using XYZ only)."""
|
|
36
|
+
return math.sqrt(sum((pose1[i] - pose2[i]) ** 2 for i in range(3)))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _sort_points_by_proximity(
|
|
40
|
+
points_db: Points, point_names: list[str], reference_name: str = "home"
|
|
41
|
+
) -> list[str]:
|
|
42
|
+
"""Sort points by distance from a reference point, then alphabetically.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
points_db: Loaded Points database.
|
|
46
|
+
point_names: List of point names to sort.
|
|
47
|
+
reference_name: Name of the reference point (default: "home").
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Sorted list of point names.
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
ref_pose = points_db[reference_name].pose
|
|
54
|
+
except KeyError:
|
|
55
|
+
# If reference point doesn't exist, use the first point
|
|
56
|
+
if point_names:
|
|
57
|
+
ref_pose = points_db[point_names[0]].pose
|
|
58
|
+
else:
|
|
59
|
+
return point_names
|
|
60
|
+
|
|
61
|
+
def sort_key(name: str):
|
|
62
|
+
try:
|
|
63
|
+
pose = points_db[name].pose
|
|
64
|
+
distance = _euclidean_distance(ref_pose, pose)
|
|
65
|
+
return (distance, name) # Sort by distance, then by name
|
|
66
|
+
except KeyError:
|
|
67
|
+
return (float("inf"), name)
|
|
68
|
+
|
|
69
|
+
return sorted(point_names, key=sort_key)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _sort_filtered_points(filtered: list[str], search_str: str) -> list[str]:
|
|
73
|
+
"""Sort filtered points by match quality: exact prefix, exact substring, then fuzzy.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
filtered: List of point names matching the search.
|
|
77
|
+
search_str: The search string.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Sorted list with best matches first.
|
|
81
|
+
"""
|
|
82
|
+
if not search_str:
|
|
83
|
+
return filtered
|
|
84
|
+
|
|
85
|
+
search_lower = search_str.lower()
|
|
86
|
+
|
|
87
|
+
# Separate into: starts with, contains, and fuzzy matches
|
|
88
|
+
starts_with = [p for p in filtered if p.lower().startswith(search_lower)]
|
|
89
|
+
contains = [p for p in filtered if search_lower in p.lower() and not p.lower().startswith(search_lower)]
|
|
90
|
+
fuzzy = [p for p in filtered if search_lower not in p.lower() and not p.lower().startswith(search_lower)]
|
|
91
|
+
|
|
92
|
+
# Sort each group alphabetically
|
|
93
|
+
starts_with.sort()
|
|
94
|
+
contains.sort()
|
|
95
|
+
|
|
96
|
+
# Sort fuzzy matches by similarity score
|
|
97
|
+
fuzzy_scored = [(p, difflib.SequenceMatcher(None, search_lower, p.lower()).ratio()) for p in fuzzy]
|
|
98
|
+
fuzzy_scored.sort(key=lambda x: (-x[1], x[0])) # Sort by score (desc), then by name
|
|
99
|
+
fuzzy = [p for p, _ in fuzzy_scored]
|
|
100
|
+
|
|
101
|
+
return starts_with + contains + fuzzy
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _explore_points(args) -> None:
|
|
105
|
+
"""Launch interactive point explorer with real-time filtering.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
args: Parsed arguments with points_path option.
|
|
109
|
+
"""
|
|
110
|
+
# Resolve points path
|
|
111
|
+
config = load_config()
|
|
112
|
+
points_path = args.points_path or config.get("points_path") or "points.db"
|
|
113
|
+
points_path = Path(points_path).resolve()
|
|
114
|
+
|
|
115
|
+
if not points_path.exists():
|
|
116
|
+
print(f"Error: Points database not found: {points_path}")
|
|
117
|
+
sys.exit(1)
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
points_db = Points(str(points_path))
|
|
121
|
+
all_points = points_db.list()
|
|
122
|
+
except Exception as e:
|
|
123
|
+
print(f"Error loading points: {e}")
|
|
124
|
+
sys.exit(1)
|
|
125
|
+
|
|
126
|
+
if not all_points:
|
|
127
|
+
print("No points saved yet.")
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
_interactive_points_filter(points_db, all_points)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _interactive_points_filter(points_db: Points, all_points: list[str]) -> None:
|
|
134
|
+
"""Interactive point explorer with real-time table filtering.
|
|
135
|
+
|
|
136
|
+
User types to filter, arrow keys scroll, ESC to quit.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
points_db: Loaded Points database.
|
|
140
|
+
all_points: List of all saved point names.
|
|
141
|
+
"""
|
|
142
|
+
# Check if stdin is a TTY (interactive terminal)
|
|
143
|
+
if not sys.stdin.isatty():
|
|
144
|
+
print("Error: Interactive mode requires a terminal.")
|
|
145
|
+
print("Run 'urkit points' in an interactive shell.")
|
|
146
|
+
sys.exit(1)
|
|
147
|
+
|
|
148
|
+
# Sort all points by proximity
|
|
149
|
+
all_points_sorted = _sort_points_by_proximity(points_db, all_points)
|
|
150
|
+
|
|
151
|
+
filter_str = ""
|
|
152
|
+
scroll = 0 # Scroll offset for viewing
|
|
153
|
+
needs_redraw = True # Flag to redraw only when needed
|
|
154
|
+
|
|
155
|
+
# Set terminal to raw mode
|
|
156
|
+
old_settings = termios.tcgetattr(sys.stdin)
|
|
157
|
+
new_settings = termios.tcgetattr(sys.stdin)
|
|
158
|
+
new_settings[3] = new_settings[3] & ~(termios.ICANON | termios.ECHO)
|
|
159
|
+
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, new_settings)
|
|
160
|
+
|
|
161
|
+
fd = sys.stdin.fileno()
|
|
162
|
+
# Rich auto-detects dark/light terminal theme
|
|
163
|
+
console = Console(force_terminal=True)
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
while True:
|
|
167
|
+
# Only redraw if something changed
|
|
168
|
+
if needs_redraw:
|
|
169
|
+
# Filter points: exact matches first, then fuzzy
|
|
170
|
+
search_lower = filter_str.lower()
|
|
171
|
+
|
|
172
|
+
# First get all exact matches (starts with or contains)
|
|
173
|
+
exact_matches = [p for p in all_points_sorted if filter_str == "" or search_lower in p.lower()]
|
|
174
|
+
|
|
175
|
+
# Then add fuzzy matches (not already in exact)
|
|
176
|
+
if filter_str:
|
|
177
|
+
fuzzy_matches = [p for p in all_points_sorted if search_lower not in p.lower()]
|
|
178
|
+
fuzzy_scored = [(p, difflib.SequenceMatcher(None, search_lower, p.lower()).ratio()) for p in fuzzy_matches]
|
|
179
|
+
fuzzy_scored = [(p, score) for p, score in fuzzy_scored if score > 0.6] # Only good matches
|
|
180
|
+
fuzzy_scored.sort(key=lambda x: -x[1]) # Sort by score
|
|
181
|
+
fuzzy_matches = [p for p, _ in fuzzy_scored]
|
|
182
|
+
filtered = exact_matches + fuzzy_matches
|
|
183
|
+
else:
|
|
184
|
+
filtered = exact_matches
|
|
185
|
+
|
|
186
|
+
# Sort by match quality (starts with, contains, fuzzy)
|
|
187
|
+
filtered = _sort_filtered_points(filtered, filter_str)
|
|
188
|
+
|
|
189
|
+
# Clamp scroll to valid range
|
|
190
|
+
if not filtered:
|
|
191
|
+
scroll = 0
|
|
192
|
+
elif scroll >= len(filtered):
|
|
193
|
+
scroll = len(filtered) - 1
|
|
194
|
+
elif scroll < 0:
|
|
195
|
+
scroll = 0
|
|
196
|
+
|
|
197
|
+
# Clear screen and draw header
|
|
198
|
+
sys.stdout.write("\033[2J\033[1;1H")
|
|
199
|
+
sys.stdout.write(cyan(" === POINT EXPLORER ===") + "\n")
|
|
200
|
+
sys.stdout.write(dim(" Type to search · ↑↓ scroll · ESC quit") + "\n\n")
|
|
201
|
+
sys.stdout.write(f" {blue('Search:')} {filter_str}\n\n")
|
|
202
|
+
|
|
203
|
+
if not filtered:
|
|
204
|
+
sys.stdout.write(yellow(f" No points match '{filter_str}'") + "\n")
|
|
205
|
+
else:
|
|
206
|
+
# Build rich table with theme-aware colors
|
|
207
|
+
table = Table(show_header=True, header_style="bold", padding=(0, 1))
|
|
208
|
+
table.add_column("Name")
|
|
209
|
+
table.add_column("X (m)", justify="right")
|
|
210
|
+
table.add_column("Y (m)", justify="right")
|
|
211
|
+
table.add_column("Z (m)", justify="right")
|
|
212
|
+
table.add_column("RX (rad)", justify="right")
|
|
213
|
+
table.add_column("RY (rad)", justify="right")
|
|
214
|
+
table.add_column("RZ (rad)", justify="right")
|
|
215
|
+
|
|
216
|
+
for point_name in filtered:
|
|
217
|
+
try:
|
|
218
|
+
point = points_db[point_name]
|
|
219
|
+
pose = point.pose
|
|
220
|
+
table.add_row(
|
|
221
|
+
point_name,
|
|
222
|
+
f"{pose[0]:7.4f}",
|
|
223
|
+
f"{pose[1]:7.4f}",
|
|
224
|
+
f"{pose[2]:7.4f}",
|
|
225
|
+
f"{pose[3]:7.4f}",
|
|
226
|
+
f"{pose[4]:7.4f}",
|
|
227
|
+
f"{pose[5]:7.4f}",
|
|
228
|
+
)
|
|
229
|
+
except Exception:
|
|
230
|
+
table.add_row(point_name, *["ERROR"] * 6)
|
|
231
|
+
|
|
232
|
+
# Capture and print table with indentation
|
|
233
|
+
import io
|
|
234
|
+
buffer = io.StringIO()
|
|
235
|
+
temp_console = Console(file=buffer, force_terminal=True)
|
|
236
|
+
temp_console.print(table)
|
|
237
|
+
table_output = buffer.getvalue()
|
|
238
|
+
for line in table_output.splitlines():
|
|
239
|
+
sys.stdout.write(" " + line + "\n")
|
|
240
|
+
|
|
241
|
+
sys.stdout.flush()
|
|
242
|
+
needs_redraw = False
|
|
243
|
+
|
|
244
|
+
# Check for input (blocking, no timeout)
|
|
245
|
+
rlist, _, _ = select.select([fd], [], [])
|
|
246
|
+
if not rlist:
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
# Read input
|
|
250
|
+
try:
|
|
251
|
+
ch = sys.stdin.read(1)
|
|
252
|
+
except Exception:
|
|
253
|
+
break
|
|
254
|
+
|
|
255
|
+
if ch == "\x1b": # ESC or arrow key
|
|
256
|
+
# Use longer timeout to reliably detect arrow sequences
|
|
257
|
+
rlist, _, _ = select.select([fd], [], [], 0.2)
|
|
258
|
+
if rlist:
|
|
259
|
+
# There's more input, likely an arrow sequence
|
|
260
|
+
try:
|
|
261
|
+
ch2 = sys.stdin.read(1)
|
|
262
|
+
if ch2 == "[":
|
|
263
|
+
ch3 = sys.stdin.read(1)
|
|
264
|
+
if ch3 == "A": # Up arrow - scroll up
|
|
265
|
+
scroll = max(0, scroll - 1)
|
|
266
|
+
needs_redraw = True
|
|
267
|
+
elif ch3 == "B": # Down arrow - scroll down
|
|
268
|
+
filtered = [p for p in all_points_sorted if filter_str == "" or filter_str.lower() in p.lower()]
|
|
269
|
+
scroll = min(len(filtered) - 1, scroll + 1) if filtered else 0
|
|
270
|
+
needs_redraw = True
|
|
271
|
+
except Exception:
|
|
272
|
+
break
|
|
273
|
+
else:
|
|
274
|
+
# No more input, so this was just ESC — quit
|
|
275
|
+
break
|
|
276
|
+
elif ch == "\x7f" or ch == "\x08": # Backspace
|
|
277
|
+
if filter_str:
|
|
278
|
+
filter_str = filter_str[:-1]
|
|
279
|
+
scroll = 0
|
|
280
|
+
needs_redraw = True
|
|
281
|
+
elif ch.isprintable():
|
|
282
|
+
filter_str += ch
|
|
283
|
+
scroll = 0
|
|
284
|
+
needs_redraw = True
|
|
285
|
+
|
|
286
|
+
except KeyboardInterrupt:
|
|
287
|
+
# Ctrl+C — exit gracefully without traceback
|
|
288
|
+
pass
|
|
289
|
+
finally:
|
|
290
|
+
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
|
|
291
|
+
sys.stdout.write("\n")
|
|
292
|
+
|
|
293
|
+
|