urkit 0.3.25__tar.gz → 0.4.0__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 (40) hide show
  1. {urkit-0.3.25 → urkit-0.4.0}/PKG-INFO +295 -140
  2. {urkit-0.3.25 → urkit-0.4.0}/README.md +294 -139
  3. {urkit-0.3.25 → urkit-0.4.0}/pyproject.toml +1 -1
  4. {urkit-0.3.25 → urkit-0.4.0}/src/urkit/__init__.py +3 -5
  5. {urkit-0.3.25 → urkit-0.4.0}/src/urkit/cli/points.py +1 -1
  6. {urkit-0.3.25 → urkit-0.4.0}/src/urkit/cli/teach.py +150 -38
  7. {urkit-0.3.25 → urkit-0.4.0}/src/urkit/config.py +7 -12
  8. {urkit-0.3.25 → urkit-0.4.0}/src/urkit/motion.py +34 -14
  9. {urkit-0.3.25 → urkit-0.4.0}/src/urkit/robot.py +234 -294
  10. {urkit-0.3.25 → urkit-0.4.0}/src/urkit/telemetry.py +0 -16
  11. {urkit-0.3.25 → urkit-0.4.0}/src/urkit.egg-info/PKG-INFO +295 -140
  12. {urkit-0.3.25 → urkit-0.4.0}/tests/test_gripper_presets.py +1 -1
  13. {urkit-0.3.25 → urkit-0.4.0}/tests/test_move_sequence.py +49 -38
  14. {urkit-0.3.25 → urkit-0.4.0}/tests/test_robot_integration.py +17 -23
  15. {urkit-0.3.25 → urkit-0.4.0}/setup.cfg +0 -0
  16. {urkit-0.3.25 → urkit-0.4.0}/src/urkit/__main__.py +0 -0
  17. {urkit-0.3.25 → urkit-0.4.0}/src/urkit/cli/__init__.py +0 -0
  18. {urkit-0.3.25 → urkit-0.4.0}/src/urkit/cli/colors.py +0 -0
  19. {urkit-0.3.25 → urkit-0.4.0}/src/urkit/cli/connection_monitor.py +0 -0
  20. {urkit-0.3.25 → urkit-0.4.0}/src/urkit/connection.py +0 -0
  21. {urkit-0.3.25 → urkit-0.4.0}/src/urkit/exceptions.py +0 -0
  22. {urkit-0.3.25 → urkit-0.4.0}/src/urkit/geometry.py +0 -0
  23. {urkit-0.3.25 → urkit-0.4.0}/src/urkit/gripper/__init__.py +0 -0
  24. {urkit-0.3.25 → urkit-0.4.0}/src/urkit/gripper/base.py +0 -0
  25. {urkit-0.3.25 → urkit-0.4.0}/src/urkit/gripper/digital.py +0 -0
  26. {urkit-0.3.25 → urkit-0.4.0}/src/urkit/gripper/presets.py +0 -0
  27. {urkit-0.3.25 → urkit-0.4.0}/src/urkit/gripper/robotiq.py +0 -0
  28. {urkit-0.3.25 → urkit-0.4.0}/src/urkit/gripper/robotiq_preamble.py +0 -0
  29. {urkit-0.3.25 → urkit-0.4.0}/src/urkit/io.py +0 -0
  30. {urkit-0.3.25 → urkit-0.4.0}/src/urkit/points.py +0 -0
  31. {urkit-0.3.25 → urkit-0.4.0}/src/urkit.egg-info/SOURCES.txt +0 -0
  32. {urkit-0.3.25 → urkit-0.4.0}/src/urkit.egg-info/dependency_links.txt +0 -0
  33. {urkit-0.3.25 → urkit-0.4.0}/src/urkit.egg-info/entry_points.txt +0 -0
  34. {urkit-0.3.25 → urkit-0.4.0}/src/urkit.egg-info/requires.txt +0 -0
  35. {urkit-0.3.25 → urkit-0.4.0}/src/urkit.egg-info/top_level.txt +0 -0
  36. {urkit-0.3.25 → urkit-0.4.0}/tests/test_exceptions.py +0 -0
  37. {urkit-0.3.25 → urkit-0.4.0}/tests/test_geometry.py +0 -0
  38. {urkit-0.3.25 → urkit-0.4.0}/tests/test_gripper.py +0 -0
  39. {urkit-0.3.25 → urkit-0.4.0}/tests/test_gripper_factory.py +0 -0
  40. {urkit-0.3.25 → urkit-0.4.0}/tests/test_points.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: urkit
3
- Version: 0.3.25
3
+ Version: 0.4.0
4
4
  Summary: Universal Robots e-Series control toolkit built on ur_rtde
5
5
  Author: URKit Contributors
6
6
  License: MIT
@@ -28,15 +28,39 @@ Requires-Dist: rich>=15
28
28
 
29
29
  [![PyPI](https://img.shields.io/pypi/v/urkit.svg)](https://pypi.org/project/urkit/)
30
30
 
31
- **URKit** is a Python toolkit for [Universal Robots](https://www.universal-robots.com/) e-Series robots that makes the common stuff simple and gets out of the way for everything else.
31
+ **URKit** makes it easy to get a Universal Robots e-Series robot moving from Python.
32
32
 
33
- Built on [`ur_rtde`](https://sdurobotics.gitlab.io/ur_rtde/), it packages the operations you reach for most: connecting, moving to named points, gripper control, telemetry, and I/O, while exposing the raw RTDE interfaces for anything deeper.
33
+ ## What it is
34
+
35
+ A thin layer over [`ur_rtde`](https://sdurobotics.gitlab.io/ur_rtde/) that handles the common stuff: connecting, teaching named points, moving between them, gripper control, telemetry. Raw RTDE interfaces are exposed for anything deeper.
36
+
37
+ No .urp files, no Dashboard API, no extra programs to run on the robot. Just connect over the network and go. Handles power-on, brake release, and RTDE setup in the constructor so you don't have to.
38
+
39
+ Comes with an interactive teach pendant CLI for positioning the robot and saving waypoints, plus a Python API for scripting motion, gripper control, I/O, and telemetry.
40
+
41
+ ## When to use this
42
+
43
+ Projects where the robot is part of something bigger. Computer vision, machine learning, sensor fusion, data logging. If your project lives in Python, keep the robot control in Python too.
44
+
45
+ Built for labs and research setups where you need to get the robot moving fast and integrate it with other software. Not designed as a drop-in replacement for Polyscope in production cells, but perfectly capable for anything that runs from a PC.
46
+
47
+ ## How it works
48
+
49
+ Use the CLI to position the robot and save named waypoints. Then reference them by name in your code: move to points, apply offsets, run sequences. Points are stored in a local SQLite database, no robot-side setup needed.
50
+
51
+ The typical workflow: teach points with the pendant, write a few lines of Python to string them together, run it. Add vision, add sensors, add logic. The robot is just one component in your pipeline.
34
52
 
35
53
  ---
36
54
 
37
55
  ## Table of Contents
38
56
 
39
57
  - [Quick Start](#quick-start)
58
+ - [Configuration](#configuration)
59
+ - [Location](#location)
60
+ - [Keys](#keys)
61
+ - [Gripper Config](#gripper-config)
62
+ - [Saving Config](#saving-config)
63
+ - [Programmatic](#programmatic)
40
64
  - [Interactive CLI](#interactive-cli)
41
65
  - [Teach Mode](#teach-mode)
42
66
  - [Points Explorer](#points-explorer)
@@ -46,8 +70,10 @@ Built on [`ur_rtde`](https://sdurobotics.gitlab.io/ur_rtde/), it packages the op
46
70
  - [Grippers](#grippers)
47
71
  - [Points & Motion](#points--motion)
48
72
  - [Telemetry](#telemetry)
73
+ - [More API](#more-api)
49
74
  - [Digital I/O](#digital-io)
50
- - [Configuration](#configuration)
75
+ - [Geometry](#geometry)
76
+ - [Robot Lifecycle](#robot-lifecycle)
51
77
  - [Advanced](#advanced)
52
78
  - [Raw RTDE Access](#raw-rtde-access)
53
79
  - [Connection Lifecycle](#connection-lifecycle)
@@ -62,7 +88,7 @@ Built on [`ur_rtde`](https://sdurobotics.gitlab.io/ur_rtde/), it packages the op
62
88
  pip install -U urkit
63
89
  ```
64
90
 
65
- The `-U` (upgrade) flag ensures you always get the latest version — this project is in early development and changes frequently.
91
+ The `-U` (upgrade) flag ensures you always get the latest version. This project is in early development and changes frequently.
66
92
 
67
93
  Requires Python 3.8+ and a Universal Robots e-Series (UR3e to UR30).
68
94
 
@@ -102,6 +128,100 @@ The typical workflow:
102
128
 
103
129
  ---
104
130
 
131
+ ## Configuration
132
+
133
+ URKit uses a YAML config file (`config.yaml`) to persist settings between sessions.
134
+
135
+ ### Location
136
+
137
+ URKit searches for `config.yaml` in the current working directory, or an explicit path via `--config`.
138
+
139
+ ### Keys
140
+
141
+ | Key | Description | Example |
142
+ |-----|-------------|---------|
143
+ | `robot_ip` | Robot IP address | `192.168.1.50` |
144
+ | `points_path` | Path to SQLite points database | `points.db` |
145
+ | `gripper` | Gripper preset name | `hand-e`, `2f-85`, `2f-140`, `digital` |
146
+ | `ik_reference` | IK reference posture (prevents elbow flipping) | `home` |
147
+ | `default_vel` | Default linear velocity (m/s) | `0.5` |
148
+ | `default_acc` | Default linear acceleration (m/s²) | `0.3` |
149
+ | `expert_mode` | Disable safety speed clamping | `false` |
150
+
151
+ ### Gripper Config
152
+
153
+ Built-in preset with overrides:
154
+
155
+ ```yaml
156
+ gripper: hand-e
157
+ gripper_config:
158
+ force: 50
159
+ speed: 80
160
+ ```
161
+
162
+ Digital I/O gripper:
163
+
164
+ ```yaml
165
+ gripper: digital
166
+ gripper_config:
167
+ pin: 3
168
+ close_on_high: true
169
+ ```
170
+
171
+ Custom gripper (arbitrary payload + TCP offset, no backend):
172
+
173
+ ```yaml
174
+ gripper:
175
+ mass: 0.5
176
+ center_of_gravity: [0.0, 0.0, 0.0]
177
+ tcp_offset: [0.0, 0.0, 0.175, 0.0, 0.0, 0.0]
178
+ backend: none
179
+ ```
180
+
181
+ Override physical properties on a built-in preset:
182
+
183
+ ```yaml
184
+ gripper: 2f-85
185
+ gripper_config:
186
+ mass: 1.2
187
+ center_of_gravity: [0.0, 0.0, 0.07]
188
+ tcp_offset: [0.0, 0.0, 0.200, 0.0, 0.0, 0.0]
189
+ ```
190
+
191
+ ### CLI Override Precedence
192
+
193
+ 1. **CLI flags.** `urkit teach 192.168.1.50 --gripper none`
194
+ 2. **Config file.** Values from `config.yaml`
195
+ 3. **Built-in defaults.** `points.db`, no gripper, 0.5 m/s velocity
196
+
197
+ ### Saving Config
198
+
199
+ The CLI **never** modifies your config file automatically. Press **Y** inside the teach pendant to save. This way you only save settings you've actually tested.
200
+
201
+ ```bash
202
+ urkit teach 192.168.1.50 --gripper hand-e # test, then press Y
203
+ urkit teach # next time: reads from config
204
+ ```
205
+
206
+ Multiple workcells:
207
+
208
+ ```bash
209
+ urkit teach --config station_a.yaml # press Y to save
210
+ urkit teach --config station_b.yaml # separate config
211
+ ```
212
+
213
+ ### Programmatic
214
+
215
+ ```python
216
+ from urkit import URRobot
217
+
218
+ robot = URRobot.from_config("config.yaml")
219
+ robot = URRobot.from_config("config.yaml", ip="10.0.0.50") # override IP
220
+ robot = URRobot.from_config({"robot_ip": "192.168.1.50", "gripper": "2f-85"}) # dict, no file
221
+ ```
222
+
223
+ ---
224
+
105
225
  ## Interactive CLI
106
226
 
107
227
  URKit provides two CLI tools: **teach** for interactive robot control, and **points** for browsing saved waypoints.
@@ -192,7 +312,6 @@ All movement and orientation keys support **hold-to-repeat**.
192
312
  <tr><td><code>V</code></td><td>Set position (mm)</td></tr>
193
313
  <tr><td><code>6</code></td><td>Set speed (0-100)</td></tr>
194
314
  <tr><td><code>7</code></td><td>Set force (0-100)</td></tr>
195
- <tr><td colspan="3">Gripper line shows: `Connected 25.0mm (50%) F=100 S=100`</td></tr>
196
315
  </table>
197
316
  </td>
198
317
  <td align="center" style="width:33%">
@@ -208,10 +327,11 @@ All movement and orientation keys support **hold-to-repeat**.
208
327
  <td align="center" style="width:34%">
209
328
  <table>
210
329
  <tr><th>Key</th><th>Action</th></tr>
211
- <tr><td><code>F</code></td><td>Freedrive (OFF → ALL → XYZ+Rz)</td></tr>
330
+ <tr><td><code>F</code></td><td>Freedrive toggle (ALL ↔ XYZ)</td></tr>
331
+ <tr><td><code>3</code></td><td>Freedrive axis menu (toggle individual axes)</td></tr>
212
332
  <tr><td><code>M</code></td><td>Toggle frame (BASE / TOOL)</td></tr>
213
333
  <tr><td><code>N</code></td><td>Go To mode (Cartesian / Joint)</td></tr>
214
- <tr><td><code>T</code></td><td>Orient TCP down (180°)</td></tr>
334
+ <tr><td><code>T</code></td><td>Open TCP orient submenu (6 directions)</td></tr>
215
335
  <tr><td><code>Y</code></td><td>Save config to file</td></tr>
216
336
  <tr><td><code>ESC</code></td><td>Exit</td></tr>
217
337
  </table>
@@ -224,10 +344,10 @@ All movement and orientation keys support **hold-to-repeat**.
224
344
  The teach pendant shows live joint angles alongside TCP position and orientation:
225
345
 
226
346
  ```
227
- Position X=+0.432 Y=+0.111 Z=+0.227
228
- Orientation R=+131.3 P=-121.0 Y= +8.0
229
- Joints J1=+150.0 J2=+020.0 J3=+160.0
230
- J4=+050.0 J5=-080.0 J6=+157.0
347
+ Position X=+0.432 Y=+0.111 Z=+0.227
348
+ Orientation R=+131.3 P=-121.0 Y= +8.0
349
+ Joints J1=+150.0 J2=+ 20.0 J3=+160.0
350
+ J4=+ 50.0 J5=- 80.0 J6=+157.0
231
351
  ```
232
352
 
233
353
  Joint angles color-code proximity to mechanical limits:
@@ -237,20 +357,20 @@ Joint angles color-code proximity to mechanical limits:
237
357
 
238
358
  UR e-Series joint limits:
239
359
 
240
- | Joint | Range | Notes |
241
- |-------|-------|-------|
242
- | J1 (shoulder pan) | ±360° | Full rotation |
243
- | J2 (shoulder lift) | ±360° | Full rotation |
244
- | J3 (elbow) | ±180° | Physically restricted — shoulder lift gets in the way |
245
- | J4 (wrist 1) | ±360° | Full rotation |
246
- | J5 (wrist 2) | ±360° | Full rotation |
247
- | J6 (wrist 3) | ±360° | Tool flange unlimited rotation |
360
+ | Joint | Range |
361
+ |-------|-------|
362
+ | J1 (shoulder pan) | ±360° |
363
+ | J2 (shoulder lift) | ±360° |
364
+ | J3 (elbow) | ±360° |
365
+ | J4 (wrist 1) | ±360° |
366
+ | J5 (wrist 2) | ±360° |
367
+ | J6 (wrist 3) | ±360° |
368
+
248
369
 
249
- Thresholds scale with each joint's range, so warning zones feel proportional across all joints.
250
370
 
251
371
  ### Safety
252
372
 
253
- By default, **Go To** and **TCP Down** movements use a slow velocity (0.125 m/s) so its safer for anyone standing near the robot. The user's speed slider still applies as a global multiplier on top of this.
373
+ By default, **Go To** and **TCP orient** movements use a slow velocity (0.125 m/s) so its safer for anyone standing near the robot. The user's speed slider still applies as a global multiplier on top of this.
254
374
 
255
375
  Delta movements (W/S/A/D/Q/E) use step-size-based velocities that scale with the speed slider set by the user.
256
376
 
@@ -289,12 +409,9 @@ robot = URRobot(
289
409
  )
290
410
  ```
291
411
 
292
- From a config file:
412
+ See [Configuration](#configuration) for `from_config()` usage.
293
413
 
294
- ```python
295
- robot = URRobot.from_config("config.yaml")
296
- robot = URRobot.from_config("config.yaml", ip="10.0.0.50") # override IP
297
- ```
414
+ The constructor takes a few seconds on first call: it validates the connection, checks remote mode, powers on the robot, releases brakes, and connects RTDE. Subsequent calls are faster if the robot is already running.
298
415
 
299
416
  ### Grippers
300
417
 
@@ -302,21 +419,25 @@ Three built-in presets:
302
419
 
303
420
  | Preset | Description |
304
421
  |--------|-------------|
305
- | `ROBOTIQ_HAND_E` | Robotiq 2F-140-E (Hand-E series) |
422
+ | `ROBOTIQ_HAND_E` | Robotiq Hand-E (2F-85-E) |
306
423
  | `ROBOTIQ_2F_85` | Robotiq 2F-85 |
307
424
  | `ROBOTIQ_2F_140` | Robotiq 2F-140 |
308
425
 
309
426
  ```python
310
427
  robot.gripper.activate() # required before open/close (Robotiq only)
428
+ robot.gripper.deactivate() # deactivate (Robotiq only)
311
429
  robot.gripper.is_activated() # check activation state
312
430
 
313
431
  robot.gripper.open() # fully open (blocking by default)
314
432
  robot.gripper.close() # fully closed, stops on contact
315
- robot.gripper.open(wait=False) # non-blocking return
316
- robot.gripper.set_position_mm(20) # 20mm open (Robotiq only, 0 = closed)
317
- robot.gripper.set_position_percent(50) # 50% open (Robotiq only, 0 = open, 100 = closed)
433
+ robot.gripper.open(wait=False) # non-blocking return (waits for position)
434
+ robot.gripper.set_position_mm(20) # 20mm open (Robotiq only, 0 = fully closed)
435
+ robot.gripper.set_position_percent(50) # 50% open (Robotiq only, 0 = fully open, 100 = fully closed)
318
436
  robot.gripper.set_force(50) # grip force: 0-100 (Robotiq only)
319
437
  robot.gripper.set_speed(80) # movement speed: 0-100 (Robotiq only)
438
+
439
+ robot.gripper.get_position_mm() # last commanded position in mm
440
+ robot.gripper.max_travel_mm() # max finger travel (e.g. 85.0 for 2F-85)
320
441
  ```
321
442
 
322
443
  Override preset values for custom fingers:
@@ -325,6 +446,19 @@ Override preset values for custom fingers:
325
446
  robot = URRobot(ip="192.168.1.50", points="points.db", gripper=ROBOTIQ_HAND_E, max_mm=120)
326
447
  ```
327
448
 
449
+ Override physical properties (e.g., custom fingers or added hardware change the weight):
450
+
451
+ ```python
452
+ robot = URRobot(
453
+ ip="192.168.1.50",
454
+ points="points.db",
455
+ gripper=ROBOTIQ_HAND_E,
456
+ mass=1.2, # override preset mass
457
+ center_of_gravity=[0.0, 0.0, 0.07], # override CoG
458
+ tcp_offset=[0.0, 0.0, 0.180, 0, 0, 0], # override TCP offset
459
+ )
460
+ ```
461
+
328
462
  #### Digital I/O Grippers
329
463
 
330
464
  Robotiq grippers use a serial protocol over the robot's RS485 port. If you have a suction cup, solenoid, or any actuator controlled by a single digital output pin, use `DigitalGripperConfig` instead. It just turns that pin on (close) and off (open).
@@ -360,6 +494,7 @@ robot.move_to("pick") # linear move (default)
360
494
  robot.move_to("pick", linear=False) # joint move
361
495
  robot.move_to("pick", vel=1.0, acc=0.5) # override speed
362
496
  robot.move_to("pick", asynchronous=True) # non-blocking, returns immediately
497
+ robot.move_to([0.5, 0, 0.3, 0, 0, 0]) # raw pose (no points DB needed)
363
498
  ```
364
499
 
365
500
  - **Linear (moveL):** TCP moves in a straight line. Predictable path, slower near complex orientations.
@@ -377,20 +512,20 @@ robot.move_to("pick", asynchronous=True)
377
512
  while robot.is_moving():
378
513
  time.sleep(0.01)
379
514
 
380
- # Or cancel mid-move
515
+ # Or cancel mid-move (see [Speed Control](#speed-control) for `stop()`)
381
516
  robot.move_to("pick", asynchronous=True)
382
517
  while robot.is_moving():
383
518
  if should_cancel:
384
- robot.stop() # sends stopL + stopJ
519
+ robot.stop()
385
520
  break
386
521
  time.sleep(0.01)
387
522
  ```
388
523
 
389
- **Teach pendant Go To** uses this pattern internally — Space cancels the move and returns to the menu.
524
+ **Teach pendant Go To** uses this pattern internally. Space cancels the move and returns to the menu.
390
525
 
391
526
  #### Pose Format
392
527
 
393
- A pose is `[x, y, z, rx, ry, rz]`: position in meters and orientation as a **rotation vector** (axis-angle in radians). This is not RPY (roll/pitch/yaw). The teach pendant displays RPY in degrees, which is a different representation. Values you see on the pendant won't match `get_tcp_pose()` directly.
528
+ A pose is `[x, y, z, rx, ry, rz]`: position in meters and orientation as a **rotation vector** (axis-angle in radians). This is not RPY (roll/pitch/yaw). The teach pendant displays RPY in degrees, which is a different representation. Values you see on the pendant won't match `get_current_point()` directly.
394
529
 
395
530
  #### Offsets
396
531
 
@@ -407,8 +542,8 @@ robot.move_to("pick", offset=[0, 0, 0.05, 0, 0.1, 0]) # full with rotation
407
542
  Get a pose without moving. Useful for logging, comparisons, or custom motion:
408
543
 
409
544
  ```python
410
- pose = robot.get_pose("pick")
411
- pose = robot.get_pose("pick", offset=[0, 0, 0.05, 0, 0, 0]) # with offset
545
+ point = robot.get_point("pick")
546
+ point = robot.get_point("pick", offset=[0, 0, 0.05, 0, 0, 0]) # with offset
412
547
  robot.move_to(pose) # move to the resolved pose later
413
548
  ```
414
549
 
@@ -424,11 +559,11 @@ robot.move_relative(delta_z=0.05) # 5cm along tool Z
424
559
  - **BASE** (default): delta relative to robot base
425
560
  - **TOOL**: delta relative to TCP orientation
426
561
 
427
- #### IK Reference (recommended)
562
+ #### IK Reference
428
563
 
429
564
  **Problem:** When the robot has multiple valid joint configurations to reach the same TCP pose (e.g., elbow up vs. elbow down), it can unexpectedly flip its posture between moves. This is called an **IK ambiguity** and it causes "weird" movements where the robot takes a strange path or flips its wrist.
430
565
 
431
- **Solution:** Set an **IK reference posture** — a saved point that defines your preferred arm configuration. The robot then stays close to that posture for all moves.
566
+ **Solution:** Set an **IK reference posture**, a saved point that defines your preferred arm configuration. The robot then stays close to that posture for all moves.
432
567
 
433
568
  ```python
434
569
  # 1. Put the robot in your preferred posture (e.g., "home")
@@ -436,7 +571,7 @@ robot.move_relative(delta_z=0.05) # 5cm along tool Z
436
571
  # 3. Set as IK reference:
437
572
  robot.ik_reference = "home"
438
573
 
439
- # Now all moves stay close to that posture — no elbow flipping
574
+ # Now all moves stay close to that posture, no elbow flipping
440
575
  robot.move_to("pick")
441
576
  robot.move_to("place")
442
577
  robot.move_relative(delta_z=-0.05)
@@ -449,7 +584,7 @@ robot_ip: 192.168.1.50
449
584
  ik_reference: home # prevents weird elbow/wrist flips
450
585
  ```
451
586
 
452
- **How it works:** The robot's inverse kinematics solver uses the reference posture as a bias (`qnear`). The TCP still reaches the exact same pose, but the arm configuration (elbow up/down, wrist orientation) stays consistent with your reference.
587
+ **How it works:** The robot's inverse kinematics solver uses the reference posture as a bias (`qnear`). The TCP still reaches the exact same pose, but the arm configuration (elbow up/down, wrist orientation) stays consistent with your reference. Under the hood, poses are resolved to joint angles and sent as `moveJ` instead of `moveL`.
453
588
 
454
589
  **Per-move override:**
455
590
 
@@ -459,14 +594,17 @@ robot.move_to("weird_pose", ik_reference=None) # one move without it
459
594
  robot.move_to("back", ik_reference="current") # use current joints
460
595
  ```
461
596
 
462
- **When to use it:** Almost always. If you've ever seen the robot move in a way that looked "wrong" or flipped its elbow unexpectedly, this is what fixes it. Set it once in config.yaml and forget about it.
597
+ **When to use it:**
463
598
 
464
- #### Points are tool-agnostic
599
+ - **Use it** for sequences of positional moves between waypoints: pick/place paths, assembly sequences, anything where the robot travels between distant points. It prevents elbow/wrist flipping.
600
+ - **Don't use it** for rotation-heavy movements (orientation adjustments, fine-tuning angles). The `qnear` seed can push joints into unexpected configs for pure rotations, and you lose the controller's native Cartesian trajectory planner (lookahead, smoothing). Set `ik_reference=None` for these moves.
465
601
 
466
- Points are stored in the active TCP frame, so they work with any tool. If you swap grippers and set the correct TCP offset, your saved points remain valid.
602
+ Default is `None` (controller handles IK natively). Set it globally when most of your moves are positional, and override per-move when you need rotations.
467
603
 
468
604
  #### Point Management
469
605
 
606
+ Points are stored in the active TCP frame, so they work with any tool. Swap grippers and your saved points stay valid.
607
+
470
608
  ```python
471
609
  robot.save_point("here")
472
610
  robot.point_names() # ["home", "pick", "place"]
@@ -481,16 +619,36 @@ robot.import_points("backup.json")
481
619
  ```python
482
620
  robot.move_relative(delta_y=0.01) # 1cm along Y
483
621
  robot.move_relative(delta_z=0.05, frame=MoveFrame.TOOL) # 5cm along tool Z
484
- robot.move_relative([0, 0.01, 0, 0, 0, 0])
622
+ robot.move_relative([0, 0.01, 0, 0, 0, 0]) # full 6-element delta
485
623
  ```
486
624
 
625
+ Individual delta parameters (`delta_x`, `delta_y`, `delta_z`, `delta_rx`, `delta_ry`, `delta_rz`) are mutually exclusive with the `delta` list. Use one or the other.
626
+
487
627
  #### Sequences
488
628
 
489
629
  ```python
490
- # Chain multiple moves into one call
491
- robot.move_sequence(["a", "b", "c"])
630
+ # Both styles work:
631
+ robot.move_sequence("a", "b", "c") # variadic
632
+ robot.move_sequence(["a", "b", "c"]) # list
633
+ ```
634
+
635
+ `move_sequence` builds a path from all targets and executes it in a single call with blending. Requires at least 2 targets. Two modes:
636
+
637
+ **With `ik_reference`** (positional paths): All poses resolve to joints using chained inverse kinematics. The first pose resolves relative to the reference, the second relative to the first's resolved joints, and so on. Executed as a single `moveJ(path)` call. Keeps the arm configuration consistent (no elbow flipping):
638
+
639
+ ```python
640
+ robot.ik_reference = "home"
641
+ robot.move_sequence(["a", "b", "c"], blend_radius=0.02) # 2cm blend
642
+ ```
643
+
644
+ **Without `ik_reference`** (Cartesian path): Builds a blended `moveL(path)`. The controller handles IK natively, better for rotation-heavy sequences where `ik_reference` causes unexpected joint behavior:
645
+
646
+ ```python
647
+ robot.move_sequence(["a", "b", "c"], blend_radius=0.02) # blended Cartesian path
492
648
  ```
493
649
 
650
+ Before sending any move, it checks that each pose has a valid IK solution. Unreachable poses raise `MotionError`. If a named point doesn't exist, it raises `PointError` instead of silently falling back.
651
+
494
652
  #### Contact Detection
495
653
 
496
654
  ```python
@@ -513,8 +671,8 @@ robot.move_until_contact(speed_z=-0.02, timeout=10.0, max_distance=0.2)
513
671
  for _ in range(3):
514
672
  if robot.move_until_contact(speed_z=-0.02, timeout=10.0, max_distance=0.2):
515
673
  break # contact detected
516
- # No contact — back off and retry
517
- robot.move_by(z=0.01)
674
+ # No contact, back off and retry
675
+ robot.move_relative(delta_z=0.01)
518
676
 
519
677
  # Manual zero (e.g. before custom force-based logic)
520
678
  robot.zero_ft_sensor()
@@ -532,40 +690,41 @@ robot.move_velocity([0, 0, -0.02, 0, 0, 0], duration=1.0)
532
690
  from urkit import FreedriveMode
533
691
 
534
692
  robot.enable_freedrive() # all 6 axes free
535
- robot.enable_freedrive(FreedriveMode.XYZ) # linear axes + Rz rotation
536
- robot.enable_freedrive(FreedriveMode.ROTATION) # rotation only
693
+ robot.enable_freedrive(FreedriveMode.XYZ) # linear axes only (X, Y, Z)
694
+ robot.enable_freedrive(FreedriveMode.ROTATION) # rotation only (Roll, Pitch, Yaw)
695
+ robot.enable_freedrive([1, 1, 1, 1, 0, 0]) # custom: X+Y+Z+Roll+Pitch
537
696
  robot.disable_freedrive() # disable before sending motion commands
538
697
  robot.is_freedrive_active # check state
539
698
  ```
540
699
 
700
+ **Teach pendant:** Press `F` to toggle freedrive (ALL ↔ XYZ). Press `3` to open the axis selection menu arrow keys navigate, space toggles each axis on/off, enter applies.
701
+
541
702
  #### Speed Control
542
703
 
543
704
  ```python
544
705
  robot.stop() # stop current move immediately (stopL + stopJ)
545
706
  robot.speed_stop() # stop velocity-controlled motion (not E-stop)
546
- robot.set_speed_slider(0.5) # 50% velocity cap
547
- robot.get_speed_slider() # read current slider (0.0-1.0)
707
+ robot.speed_slider = 0.5 # 50% velocity cap
708
+ robot.speed_slider # read current slider (0.0-1.0)
548
709
  ```
549
710
 
550
711
  The speed slider controls the pendant's speed multiplier. It's global, persistent, and affects all motion commands.
551
712
 
552
713
  #### Changing Default Speed & Acceleration
553
714
 
554
- Override the constructor defaults at runtime — all subsequent moves pick up the new values:
715
+ Override the constructor defaults at runtime. All subsequent moves pick up the new values:
555
716
 
556
717
  ```python
557
- robot.set_speed(0.1) # slow for precision work
718
+ robot.default_vel = 0.1 # slow for precision work
558
719
  robot.move_to("insert")
559
- robot.set_speed(0.5) # back to normal
720
+ robot.default_vel = 0.5 # back to normal
560
721
 
561
- robot.set_acc(0.05) # gentle acceleration
722
+ robot.default_acc = 0.05 # gentle acceleration
562
723
 
563
724
  robot.default_vel # read current velocity (m/s)
564
725
  robot.default_acc # read current acceleration (m/s²)
565
726
  ```
566
727
 
567
- Soft warnings log when values exceed typical robot limits (> 2 m/s for velocity, > 6 m/s² for acceleration). The controller may clamp aggressive values based on payload and configuration.
568
-
569
728
  #### Inverse Kinematics
570
729
 
571
730
  ```python
@@ -575,121 +734,124 @@ joints = robot.inverse_kinematics([0.5, 0, 0.3, 0, 0, 0])
575
734
  ### Telemetry
576
735
 
577
736
  ```python
578
- pose = robot.get_tcp_pose() # [x, y, z, rx, ry, rz]
579
- joints = robot.get_joint_positions() # [j0..j5]
580
- force = robot.get_tcp_force() # [fx, fy, fz, mx, my, mz]
581
- mode = robot.get_robot_mode() # "REMOTE_CONTROL", "SERVOING", etc.
582
- payload = robot.get_payload() # kg
583
- robot.is_moving() # bool — any joint/TCP velocity non-zero
584
- robot.is_protective_stopped() # bool
585
- robot.is_emergency_stopped() # bool
586
- robot.current_point() # {"pose": [...], "joints": [...]}
587
- robot.is_at_pose(target) # bool — TCP within 1mm / 0.5°
588
- robot.is_at_joints(target) # bool — all joints within 0.001 rad
737
+ pose = robot.get_current_point() # [x, y, z, rx, ry, rz]
738
+ pose = robot.get_current_point(offset_z=0.05) # with offset
739
+ joints = robot.get_joint_positions() # [j0..j5]
740
+ force = robot.get_tcp_force() # [fx, fy, fz, mx, my, mz]
741
+ mode = robot.get_robot_mode() # "REMOTE_CONTROL", "SERVOING", etc.
742
+ payload = robot.payload # kg
743
+ robot.is_protective_stopped() # bool
744
+ robot.is_emergency_stopped() # bool
589
745
  ```
590
746
 
591
- `is_moving()` is the simplest way to poll — `is_at_pose()` / `is_at_joints()` for precise target checking:
747
+ #### Arrival Detection
748
+
749
+ `is_moving()` tracks the target pose or joints stored by `move_to()` and compares against the current position. Returns `False` when within tolerance:
592
750
 
593
751
  ```python
594
752
  robot.move_to("pick", asynchronous=True)
595
753
 
596
- # Simple: wait until robot stops
754
+ # Wait until arrived (default tolerances: 2mm position, ~2° orientation)
597
755
  while robot.is_moving():
598
756
  time.sleep(0.01)
599
757
 
600
- # Precise: check against specific target
601
- while not robot.is_at_pose(target_pose):
758
+ # Tighter tolerances
759
+ while robot.is_moving(position_tolerance=0.001, orientation_tolerance=0.017):
760
+ time.sleep(0.01)
761
+
762
+ # Joint moves, tighter joint tolerance
763
+ while robot.is_moving(joint_tolerance=0.001):
602
764
  time.sleep(0.01)
603
765
  ```
604
766
 
767
+ For joint-space moves (when `ik_reference` is active or `linear=False`), `is_moving()` compares joint angles. For Cartesian moves, it compares TCP pose. When no target is set (e.g., after a relative move without `move_to`), it returns `False`.
768
+
769
+ ---
770
+
771
+ ## More API
772
+
773
+ Less common but useful when you need them.
774
+
605
775
  ### Digital I/O
606
776
 
777
+ Pins 0–7 are standard, 8–15 configurable, 16–17 tool.
778
+
607
779
  ```python
780
+ # Outputs
608
781
  robot.set_digital_output(0, True)
609
782
  robot.set_digital_outputs({0: True, 1: False, 8: True})
610
- robot.set_digital_outputs(False) # clear all
783
+ robot.set_digital_outputs(False) # clear all pins 0–15
784
+ robot.get_digital_output(0) # read back output state
611
785
 
786
+ # Inputs
612
787
  robot.get_digital_input(0)
613
- robot.get_analog_input(0)
614
- robot.get_tool_input(0)
615
-
616
788
  robot.wait_for_input(0, True, timeout=10.0) # block until pin 0 goes high
617
- ```
618
-
619
- ---
620
789
 
621
- ## Configuration
622
-
623
- URKit uses a YAML config file (`config.yaml`) to persist settings between sessions.
790
+ # Analog
791
+ robot.get_analog_input(0) # read analog input (pin 0–1)
792
+ robot.get_analog_output(0) # read analog output (pin 0–1)
624
793
 
625
- ### Location
794
+ # Tool I/O
795
+ robot.get_tool_input(0) # tool digital input (pin 0–1)
796
+ robot.get_tool_output(0) # tool digital output (pin 0–1)
797
+ ```
626
798
 
627
- URKit searches for `config.yaml` in this order:
628
- 1. Explicit path via `--config` flag or `load_config("path")`
629
- 2. Project root (where `src/urkit` lives)
630
- 3. Current working directory
799
+ ### Geometry
631
800
 
632
- ### Keys
801
+ Conversion utilities for rotation vectors, quaternions, and RPY angles (rxyz convention, same as UR teach pendant):
633
802
 
634
- | Key | Description | Example |
635
- |-----|-------------|---------|
636
- | `robot_ip` | Robot IP address | `192.168.1.50` |
637
- | `points_path` | Path to SQLite points database | `points.db` |
638
- | `gripper` | Gripper preset name | `hand-e`, `2f-85`, `2f-140`, `digital` |
639
- | `ik_reference` | IK reference posture (prevents elbow flipping) | `home` |
640
- | `default_vel` | Default linear velocity (m/s) | `0.5` |
641
- | `default_acc` | Default linear acceleration (m/s²) | `0.3` |
642
- | `expert_mode` | Disable safety speed clamping | `false` |
803
+ ```python
804
+ from urkit import (
805
+ orient_tcp,
806
+ orient_tcp_down,
807
+ quat_to_rotvec,
808
+ quat_to_rpy,
809
+ rpy_to_quat,
810
+ rotvec_to_quat,
811
+ )
643
812
 
644
- ### Gripper Config
813
+ # Orient TCP along an arbitrary direction (minimal rotation)
814
+ new_pose = orient_tcp(current_pose, [0, 0, -1]) # point down
815
+ new_pose = orient_tcp(current_pose, [1, 0, 0]) # point forward
645
816
 
646
- ```yaml
647
- gripper: digital
648
- gripper_config:
649
- pin: 3
650
- close_on_high: true
651
- ```
817
+ # Orient TCP straight down (convenience wrapper)
818
+ new_pose = orient_tcp_down(current_pose)
652
819
 
653
- ```yaml
654
- gripper: hand-e
655
- gripper_config:
656
- force: 50
657
- speed: 80
820
+ # Rotation conversions
821
+ q = rotvec_to_quat([0, 0, 1.57]) # rotvec → quaternion (x, y, z, w)
822
+ rv = quat_to_rotvec(q) # quaternion → rotvec
823
+ rpy = quat_to_rpy(q) # quaternion → RPY (radians)
824
+ q = rpy_to_quat(0, 0, 1.57) # RPY → quaternion
658
825
  ```
659
826
 
660
- ### CLI Override Precedence
661
-
662
- 1. **CLI flags.** `urkit teach 192.168.1.50 --gripper none`
663
- 2. **Config file.** Values from `config.yaml`
664
- 3. **Built-in defaults.** `points.db`, no gripper, 0.5 m/s velocity
827
+ ### Robot Lifecycle
665
828
 
666
- ### Saving Config
667
-
668
- The CLI **never** modifies your config file automatically. Press **Y** inside the teach pendant to save. This way you only save settings you've actually tested.
829
+ Manual power and brake control via the Dashboard:
669
830
 
670
- ```bash
671
- urkit teach 192.168.1.50 --gripper hand-e # test, then press Y
672
- urkit teach # next time: reads from config
831
+ ```python
832
+ robot.power_on() # power on (skips if already on)
833
+ robot.release_brakes() # release brakes (enable control)
834
+ robot.recover() # clear protective stop
835
+ robot.power_off() # power off
673
836
  ```
674
837
 
675
- Multiple workcells:
838
+ The constructor handles all of this automatically. Use these methods when you need fine-grained control (e.g., recovering from a safety stop without recreating the robot).
676
839
 
677
- ```bash
678
- urkit teach --config station_a.yaml # press Y to save
679
- urkit teach --config station_b.yaml # separate config
840
+ TCP and payload can be set manually (gripper presets do this automatically):
841
+
842
+ ```python
843
+ robot.tcp_offset = [0, 0, 0.15, 0, 0, 0]
844
+ robot.set_payload(1.5, [0, 0, 0.05]) # mass (kg), center of gravity [x, y, z]
680
845
  ```
681
846
 
682
- ### Programmatic
847
+ Clean shutdown:
683
848
 
684
849
  ```python
685
- from urkit import load_config, resolve_config
686
-
687
- config = load_config() # auto-resolve
688
- config = load_config("/path/to/my.yaml") # explicit path
689
- path = resolve_config() # returns Path or None
690
- robot = URRobot.from_config({"robot_ip": "192.168.1.50", "gripper": "2f-85"})
850
+ robot.disconnect() # close RTDE, Dashboard, points DB, gripper
691
851
  ```
692
852
 
853
+ `disconnect()` is called automatically on garbage collection. Call it explicitly to release resources immediately.
854
+
693
855
  ---
694
856
 
695
857
  ## Advanced
@@ -713,8 +875,6 @@ robot.connection_lost # bool: check if RTDE dropped
713
875
  robot.reconnect_rtde() # reconnect after a drop
714
876
  ```
715
877
 
716
- `disconnect()` is called automatically when the robot object is garbage collected.
717
-
718
878
  ### Error Handling
719
879
 
720
880
  ```python
@@ -742,13 +902,8 @@ Common runtime errors:
742
902
 
743
903
  When the robot enters protective stop or the RTDE connection drops, motion commands raise `URKitConnectionError` and the program should exit. The CLI handles this automatically.
744
904
 
745
- ### Connection Notes
746
-
747
- The `URRobot` constructor takes a few seconds on first call: it validates the connection, checks remote mode, powers on the robot, releases brakes, and connects RTDE. Subsequent calls are faster if the robot is already running.
748
-
749
905
  ---
750
906
 
751
907
  ## Changelog
752
908
 
753
909
  See [CHANGELOG.md](CHANGELOG.md) for the full version history.
754
-