urkit 0.5.0__tar.gz → 0.6.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 (52) hide show
  1. {urkit-0.5.0 → urkit-0.6.0}/PKG-INFO +124 -20
  2. {urkit-0.5.0 → urkit-0.6.0}/README.md +123 -19
  3. {urkit-0.5.0 → urkit-0.6.0}/pyproject.toml +1 -1
  4. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/__init__.py +13 -1
  5. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/__main__.py +6 -3
  6. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/cli/init.py +9 -2
  7. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/cli/points.py +1 -2
  8. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/cli/teach.py +280 -147
  9. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/cli/terminal.py +0 -14
  10. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/config.py +61 -5
  11. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/gripper/__init__.py +14 -0
  12. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/gripper/base.py +4 -1
  13. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/gripper/digital.py +15 -0
  14. urkit-0.6.0/src/urkit/gripper/epick.py +795 -0
  15. urkit-0.6.0/src/urkit/gripper/presets.py +274 -0
  16. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/gripper/robotiq.py +13 -0
  17. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/robot.py +178 -56
  18. {urkit-0.5.0 → urkit-0.6.0}/src/urkit.egg-info/PKG-INFO +124 -20
  19. {urkit-0.5.0 → urkit-0.6.0}/src/urkit.egg-info/SOURCES.txt +3 -0
  20. urkit-0.6.0/tests/test_config.py +100 -0
  21. {urkit-0.5.0 → urkit-0.6.0}/tests/test_gripper_factory.py +137 -2
  22. urkit-0.6.0/tests/test_gripper_presets.py +285 -0
  23. urkit-0.6.0/tests/test_held_payload.py +161 -0
  24. {urkit-0.5.0 → urkit-0.6.0}/tests/test_robot_integration.py +1 -1
  25. urkit-0.5.0/src/urkit/gripper/presets.py +0 -113
  26. urkit-0.5.0/tests/test_gripper_presets.py +0 -152
  27. {urkit-0.5.0 → urkit-0.6.0}/setup.cfg +0 -0
  28. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/cli/__init__.py +0 -0
  29. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/cli/colors.py +0 -0
  30. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/cli/connection_monitor.py +0 -0
  31. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/connection.py +0 -0
  32. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/exceptions.py +0 -0
  33. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/geometry.py +0 -0
  34. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/gripper/robotiq_preamble.py +0 -0
  35. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/io.py +0 -0
  36. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/motion.py +0 -0
  37. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/points.py +0 -0
  38. {urkit-0.5.0 → urkit-0.6.0}/src/urkit/telemetry.py +0 -0
  39. {urkit-0.5.0 → urkit-0.6.0}/src/urkit.egg-info/dependency_links.txt +0 -0
  40. {urkit-0.5.0 → urkit-0.6.0}/src/urkit.egg-info/entry_points.txt +0 -0
  41. {urkit-0.5.0 → urkit-0.6.0}/src/urkit.egg-info/requires.txt +0 -0
  42. {urkit-0.5.0 → urkit-0.6.0}/src/urkit.egg-info/top_level.txt +0 -0
  43. {urkit-0.5.0 → urkit-0.6.0}/tests/test_connection_gate.py +0 -0
  44. {urkit-0.5.0 → urkit-0.6.0}/tests/test_exceptions.py +0 -0
  45. {urkit-0.5.0 → urkit-0.6.0}/tests/test_freedrive_cycle.py +0 -0
  46. {urkit-0.5.0 → urkit-0.6.0}/tests/test_geometry.py +0 -0
  47. {urkit-0.5.0 → urkit-0.6.0}/tests/test_gripper.py +0 -0
  48. {urkit-0.5.0 → urkit-0.6.0}/tests/test_ik_reference.py +0 -0
  49. {urkit-0.5.0 → urkit-0.6.0}/tests/test_message_line.py +0 -0
  50. {urkit-0.5.0 → urkit-0.6.0}/tests/test_move_sequence.py +0 -0
  51. {urkit-0.5.0 → urkit-0.6.0}/tests/test_points.py +0 -0
  52. {urkit-0.5.0 → urkit-0.6.0}/tests/test_terminal_input.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: urkit
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Universal Robots e-Series control toolkit built on ur_rtde
5
5
  Author: URKit Contributors
6
6
  License: MIT
@@ -96,8 +96,8 @@ Requires Python 3.8+ and a Universal Robots e-Series (UR3e to UR30).
96
96
 
97
97
  ### Platform Support
98
98
 
99
- - **Linux, macOS**: fully supported
100
- - **Windows**: the Python library works on Windows. The interactive CLIs (`urkit teach`, `urkit points`) run on Windows but are **not fully tested and not officially supported**. A warning is printed at startup. If you hit input or console problems, use Linux, macOS, or WSL2
99
+ - **Linux, Windows**: fully supported. The interactive CLIs (`urkit teach`, `urkit points`) are tested on both platforms
100
+ - **macOS**: runs the same POSIX terminal code path as Linux (termios/select); there is no macOS-specific code
101
101
 
102
102
  ### Robot Setup (one-time)
103
103
 
@@ -138,7 +138,7 @@ That's it. No `.urp` files to run, no extra programs needed.
138
138
  If you have a Robotiq gripper, install the **Robotiq Gripper Control** URCap first: download from [robotiq.com/support](https://robotiq.com/support), copy the `.urcap` to a USB drive, and install via `☰` → `Settings` → `System` → `URCaps`.
139
139
 
140
140
  ```python
141
- from urkit import URRobot, ROBOTIQ_HAND_E # or ROBOTIQ_2F_85, ROBOTIQ_2F_140, or gripper=None
141
+ from urkit import URRobot, ROBOTIQ_HAND_E # or ROBOTIQ_2F_85, ROBOTIQ_2F_140, ROBOTIQ_EPICK_SINGLE_S, or gripper=None
142
142
 
143
143
  robot = URRobot(ip="192.168.1.50", points="points.db", gripper=ROBOTIQ_HAND_E)
144
144
  robot.gripper.activate()
@@ -146,8 +146,10 @@ robot.gripper.activate()
146
146
  robot.move_to("home")
147
147
  robot.move_to("pick", dz=0.05) # 5cm above
148
148
  robot.gripper.close()
149
+ robot.set_held_payload(0.05) # picked up 50 g: arm compensates gravity
149
150
  robot.move_to("place")
150
151
  robot.gripper.open()
152
+ robot.set_held_payload(0) # released: back to gripper-only payload
151
153
  ```
152
154
 
153
155
  The typical workflow:
@@ -195,12 +197,14 @@ The CLI auto-loads `config.yaml` from the current working directory. Pass `--con
195
197
  |-----|-------------|---------|
196
198
  | `robot_ip` | Robot IP address | `192.168.1.50` |
197
199
  | `points_path` | Path to SQLite points database | `points.db` |
198
- | `gripper` | Gripper preset name | `hand-e`, `2f-85`, `2f-140`, `digital` |
200
+ | `gripper` | Gripper preset name | `hand-e`, `2f-85`, `2f-140`, `epick-single-s`, `digital` |
199
201
  | `ik_reference` | IK reference posture (prevents elbow flipping) | `home` |
200
202
  | `default_vel` | Default linear velocity (m/s) | `0.5` |
201
203
  | `default_acc` | Default linear acceleration (m/s²) | `0.3` |
202
204
  | `expert_mode` | Disable safety speed clamping | `false` |
203
205
 
206
+ The loader fails loudly with a reason when a config file exists but is wrong: malformed YAML (file + parse error), a document that is not a mapping, or unknown top-level keys (offending keys + the valid ones; usually a typo). The table above is the complete key set. The only silent case is a missing file, where the CLI runs on flags and built-in defaults.
207
+
204
208
  ### Gripper Config
205
209
 
206
210
  Built-in preset with overrides:
@@ -291,8 +295,6 @@ urkit init --force # overwrite if exists
291
295
 
292
296
  ### Teach Mode
293
297
 
294
- ### Teach Mode
295
-
296
298
  The interactive teach pendant for moving the robot, saving points, and checking telemetry:
297
299
 
298
300
  ```bash
@@ -305,10 +307,10 @@ urkit teach # reads IP from config.yaml
305
307
  | Flag | Description |
306
308
  |------|-------------|
307
309
  | `ip` | Robot IP address (positional, overrides config) |
308
- | `--gripper` | Gripper preset: `2f-85`, `2f-140`, `hand-e`, `digital`, `none` |
310
+ | `--gripper` | Gripper preset: `2f-85`, `2f-140`, `hand-e`, `epick-single-s`, `digital`, `none` |
309
311
  | `--gripper-pin` | Digital gripper output pin (default: 0) |
310
- | `--gripper-force` | Robotiq force 0-100 |
311
- | `--gripper-speed` | Robotiq speed 0-100 |
312
+ | `--gripper-force` | 2F grip force 0-100 (EPick: use `max_vacuum` in `gripper_config`) |
313
+ | `--gripper-speed` | 2F movement speed 0-100 (EPick: use `grip_timeout` in `gripper_config`) |
312
314
  | `--gripper-close-on-high` | Digital polarity: `true` or `false` |
313
315
  | `--points` | Path to `points.db` file (overrides config) |
314
316
  | `--config` | Path to config file (default: `config.yaml` in project root or CWD) |
@@ -374,9 +376,10 @@ All movement and orientation keys support **hold-to-repeat**.
374
376
  <tr><th>Key</th><th>Action</th></tr>
375
377
  <tr><td><code>X</code></td><td>Open</td></tr>
376
378
  <tr><td><code>C</code></td><td>Close</td></tr>
377
- <tr><td><code>V</code></td><td>Set position (mm)</td></tr>
378
- <tr><td><code>6</code></td><td>Set speed (0-100)</td></tr>
379
- <tr><td><code>7</code></td><td>Set force (0-100)</td></tr>
379
+ <tr><td><code>V</code></td><td>Set position % (2F only; hidden for the EPick)</td></tr>
380
+ <tr><td><code>6</code></td><td>Set speed (2F) / min vacuum (EPick)</td></tr>
381
+ <tr><td><code>7</code></td><td>Set force (2F) / max vacuum (EPick)</td></tr>
382
+ <tr><td><code>9</code></td><td>Set held payload (kg, 0 = reset to base)</td></tr>
380
383
  </table>
381
384
  </td>
382
385
  <td align="center" style="width:33%">
@@ -409,10 +412,10 @@ All movement and orientation keys support **hold-to-repeat**.
409
412
  The teach pendant shows live joint angles alongside TCP position and orientation:
410
413
 
411
414
  ```
412
- Position X=+0.432 Y=+0.111 Z=+0.227
413
- Orientation R=+131.3 P=-121.0 Y= +8.0
414
- Joints J1=+150.0 J2=+ 20.0 J3=+160.0
415
- J4=+ 50.0 J5=- 80.0 J6=+157.0
415
+ Position (mm) X= +432.0 Y= +111.0 Z= +227.0
416
+ Orientation R= +131.3 P= -121.0 Y= +8.0
417
+ Joints J1=+150.0 J2= +20.0 J3=+160.0
418
+ J4= +50.0 J5= -80.0 J6=+157.0
416
419
  ```
417
420
 
418
421
  Joint angles color-code proximity to mechanical limits:
@@ -480,13 +483,16 @@ The constructor takes a few seconds on first call: it validates the connection,
480
483
 
481
484
  ### Grippers
482
485
 
483
- Three built-in presets:
486
+ Built-in presets:
484
487
 
485
488
  | Preset | Description |
486
489
  |--------|-------------|
487
490
  | `ROBOTIQ_HAND_E` | Robotiq Hand-E (2F-85-E) |
488
491
  | `ROBOTIQ_2F_85` | Robotiq 2F-85 |
489
492
  | `ROBOTIQ_2F_140` | Robotiq 2F-140 |
493
+ | `ROBOTIQ_EPICK_SINGLE_S/L` | EPick + 1 cup (S = Ø40, L = Ø55) |
494
+ | `ROBOTIQ_EPICK_DUAL_S/L` | EPick + 2-cup bracket (S = Ø40, L = Ø55) |
495
+ | `ROBOTIQ_EPICK_QUAD_S/L` | EPick + 4-cup bracket (S = Ø40, L = Ø55) |
490
496
 
491
497
  ```python
492
498
  robot.gripper.activate() # required before open/close (Robotiq only)
@@ -497,7 +503,7 @@ robot.gripper.open() # fully open (blocking by default)
497
503
  robot.gripper.close() # fully closed, stops on contact
498
504
  robot.gripper.open(wait=False) # non-blocking return (waits for position)
499
505
  robot.gripper.set_position_mm(20) # 20mm open (Robotiq only, 0 = fully closed)
500
- robot.gripper.set_position_percent(50) # 50% open (Robotiq only, 0 = fully open, 100 = fully closed)
506
+ robot.gripper.set_position_percent(50) # 50% open (2F; repurposed on the EPick, see below)
501
507
  robot.gripper.set_force(50) # grip force: 0-100 (Robotiq only)
502
508
  robot.gripper.set_speed(80) # movement speed: 0-100 (Robotiq only)
503
509
 
@@ -543,6 +549,83 @@ robot.gripper.close() # turn pin 3 on
543
549
 
544
550
  `set_force()` and `set_speed()` are not available for digital grippers.
545
551
 
552
+ #### EPick Vacuum Gripper
553
+
554
+ The EPick presets drive a Robotiq EPick vacuum gripper (e-Series only) over the same RS485 Robotiq protocol as the 2F series, using the legacy Robotiq ASCII commands. They require the **Robotiq Gripper Control** URCap installed (same setup as the 2F presets).
555
+
556
+ The semantics are inverted compared to the 2F grippers:
557
+
558
+ ```python
559
+ robot.gripper.activate() # required before open/close
560
+ robot.gripper.close() # grip: pump on, wait until an object is secured
561
+ robot.gripper.open() # release: pump off + blow-off, wait until the object is gone
562
+ ```
563
+
564
+ `close()` does not return until the gripper has confirmed a secured object (see Detection below), and raises `GripperError` if no object is detected within the grip timeout. A failed grip leaves the vacuum on, so call `open()` to release before moving again. `open()` waits until the vacuum is lost (the object has dropped).
565
+
566
+ Grip parameters (set at connect time, or changed between grips):
567
+
568
+ ```python
569
+ robot.gripper.set_max_vacuum(60) # max vacuum level, 20-100% (default 60, the Robotiq URCap default)
570
+ robot.gripper.set_min_vacuum(40) # min vacuum level, 10% up to the max vacuum (default 40)
571
+ robot.gripper.set_grip_timeout(30) # grip timeout in 100 ms units, 0-100 (default 30 = 3 s)
572
+ ```
573
+
574
+ The ranges are enforced at construction and on every setter (max 20-100, min 10 up to the current max, timeout 0-100), and a parameter of one backend passed to another raises `GripperError` instead of being ignored (`force`/`speed` on the EPick, `max_vacuum`/`grip_timeout` on the 2F). The same names work in `config.yaml`:
575
+
576
+ ```yaml
577
+ gripper: epick-single-s
578
+ gripper_config:
579
+ max_vacuum: 70
580
+ grip_timeout: 40
581
+ ```
582
+
583
+ With `set_max_vacuum(100)` the pump runs continuously and the gripper cannot confirm a grip; keep the max vacuum below 100% (60 works for typical cups and trays). The same physics applies at lower levels: if the max vacuum is higher than a leaky seal can reach, the pump keeps hunting and never enters the stable hold phase where the secured flag appears, so the grip times out (verified: a marginal-seal object gripped at 60/40 but timed out at 80/50 and 80/30). For leaky seals keep `max_vacuum` low and `min_vacuum` at or below what the seal sustains, not the other way around.
584
+
585
+ `set_position_mm()`, `get_position_mm()`, and `max_travel_mm()` are not available: the EPick has no finger travel. `set_position_percent(p)` is repurposed as a one-shot vacuum grip: `p=0` releases, `1-100` grips at that vacuum level with the normal secured-object wait (the teach pendant does not expose this; use [6]/[7] and C).
586
+
587
+ **Payload and TCP.** The bare generator body is 0.706 kg, CoG `[0, 0, 0.0515]`, TCP `[0, 0, 0.113, 0, 0, 0]` (Robotiq EPick manual, Table 6-4); cups and brackets change all three. The kits ship cups in two sizes (Ø 40 mm and Ø 55 mm), so there is one preset per bracket system + cup size (configurations without cups cannot grip and have no preset):
588
+
589
+ | Preset | Config / CLI name | Setup |
590
+ |--------|-------------------|-------|
591
+ | `ROBOTIQ_EPICK_SINGLE_S` | `epick-single-s` | 1 small (Ø40) cup on the generator port |
592
+ | `ROBOTIQ_EPICK_SINGLE_L` | `epick-single-l` | 1 large (Ø55) cup on the generator port |
593
+ | `ROBOTIQ_EPICK_DUAL_S` | `epick-dual-s` | 2-cup bracket, small cups |
594
+ | `ROBOTIQ_EPICK_DUAL_L` | `epick-dual-l` | 2-cup bracket, large cups |
595
+ | `ROBOTIQ_EPICK_QUAD_S` | `epick-quad-s` | 4-cup bracket, small cups |
596
+ | `ROBOTIQ_EPICK_QUAD_L` | `epick-quad-l` | 4-cup bracket, large cups |
597
+
598
+ ```python
599
+ from urkit import URRobot, ROBOTIQ_EPICK_SINGLE_S
600
+
601
+ robot = URRobot(ip="192.168.1.50", gripper=ROBOTIQ_EPICK_SINGLE_S)
602
+ ```
603
+
604
+ The TCP z of each preset is the bottom of the cups, the limiting point for approach moves. The presets use the manual's Table 6-4 for the body and bracket stack, and the official Festo datasheets for the cups Robotiq ships (VASB-40-1/4-PUR-B: 30 g / 32 mm, VASB-55-1/4-PUR-B: 39 g / 39 mm); all-small combinations reproduce Table 6-4 exactly:
605
+
606
+ | Preset | Mass (kg) | CoG z (m) | TCP z (m) |
607
+ |--------|-----------|-----------|-----------|
608
+ | body only (reference) | 0.706 | 0.0515 | 0.113 |
609
+ | `ROBOTIQ_EPICK_SINGLE_S` | 0.736 | 0.0547 | 0.145 |
610
+ | `ROBOTIQ_EPICK_SINGLE_L` | 0.745 | 0.0557 | 0.152 |
611
+ | `ROBOTIQ_EPICK_DUAL_S` | 1.116 | 0.0819 | 0.196 |
612
+ | `ROBOTIQ_EPICK_DUAL_L` | 1.134 | 0.0837 | 0.203 |
613
+ | `ROBOTIQ_EPICK_QUAD_S` | 1.341 | 0.0920 | 0.196 |
614
+ | `ROBOTIQ_EPICK_QUAD_L` | 1.377 | 0.0947 | 0.203 |
615
+
616
+ Bracket mass is derived (±15 g) and the Ø55 height is CAD-derived (±2 mm); if your setup is critical, measure the actual stack and pass `mass` / `center_of_gravity` / `tcp_offset` overrides, which still work:
617
+
618
+ ```python
619
+ robot = URRobot(
620
+ ip="192.168.1.50",
621
+ gripper=ROBOTIQ_EPICK_SINGLE_S,
622
+ mass=0.74,
623
+ tcp_offset=[0.0, 0.0, 0.147, 0.0, 0.0, 0.0],
624
+ )
625
+ ```
626
+
627
+ **Detection.** The EPick runs in advanced mode (rMOD=1): the pump reaches the max vacuum level, stops, and the vacuum decays back to the min level. An object is only "secured" while the vacuum is actually held there, so `close()` returns as soon as that secured state is observed on the gripper's object flag. With nothing under the suction cup the vacuum collapses and the secured state never occurs, so `close()` times out and raises `GripperError` after the grip timeout (set via `grip_timeout`). In automatic mode (rMOD=0) the EPick reports a short "seemingly constant" vacuum as an object, which false-positives with no object on the cup, so urkit does not use it.
628
+
546
629
  ### Points & Motion
547
630
 
548
631
  The points database is optional. Create a robot without one and attach later:
@@ -773,7 +856,7 @@ robot.speed_slider = 0.5 # 50% velocity cap
773
856
  robot.speed_slider # read current slider (0.0-1.0)
774
857
  ```
775
858
 
776
- The speed slider controls the pendant's speed multiplier. It's global, persistent, and affects all motion commands.
859
+ The speed slider controls the pendant's speed multiplier. It's global, persistent (stored on the robot, survives restarts), and affects all motion commands. Because 0% persists as a silent freeze of all motion, the pendant's `[0]` prompt rejects 0 (1-100 only); a deliberate freeze is still possible via the API (`robot.speed_slider = 0`).
777
860
 
778
861
  #### Changing Default Speed & Acceleration
779
862
 
@@ -909,6 +992,27 @@ robot.tcp_offset = [0, 0, 0.15, 0, 0, 0]
909
992
  robot.set_payload(1.5, [0, 0, 0.05]) # mass (kg), center of gravity [x, y, z]
910
993
  ```
911
994
 
995
+ **Held payload.** The controller compensates gravity using the payload, so add the mass of a picked-up object before moving it, and reset after release:
996
+
997
+ ```python
998
+ robot.gripper.close()
999
+ robot.set_held_payload(0.05) # holding 50 g, object CoG at the TCP
1000
+ robot.set_held_payload(0.05, cog=[0, 0, 0.25]) # object hangs below the flange
1001
+ robot.set_held_payload(0) # released: back to the base payload
1002
+
1003
+ robot.base_payload # gripper + tooling, kg (no held object)
1004
+ robot.held_payload # currently held mass, kg (0 = none)
1005
+ robot.payload # total (base + held), kg
1006
+ ```
1007
+
1008
+ `set_held_payload(0)` resets to the base payload captured at connect (the gripper preset plus any `mass=` / `center_of_gravity` / `tcp_offset` overrides), never to a literal zero. The combined CoG is the mass-weighted average of the base CoG and the object's CoG (tool coordinates, origin at the flange). A held object that extends the tool does not need a TCP change: give its CoG the deeper z instead.
1009
+
1010
+ The base payload can be changed at runtime with `set_payload(...)`: while nothing is held, a manual `set_payload` call becomes the new base (custom fingers, a re-weighed tool). While a payload is held, `set_payload` raises, since the mass would be ambiguous: call `set_held_payload(0)` first.
1011
+
1012
+ In the teach pendant, **9** sets the held payload (prompt: `Held payload in kg (0 = reset to base)`); the Payload status row shows the held mass and hints the key, e.g. `1.427kg +0.050 held [9]`. Useful when teaching points while gripping a real part.
1013
+
1014
+ **How much to report.** There is no enforced minimum, and over-reporting is harmless. Practical guidance: below ~10 g the sag is sub-millimeter on any e-Series arm and not worth it; from ~50 g up (roughly 1% of a UR5e's rated payload) report it, since full-reach sag reaches several mm; in between, report it for slow precision work (insertion, contact moves) and skip it for coarse moves.
1015
+
912
1016
  Clean shutdown:
913
1017
 
914
1018
  ```python
@@ -70,8 +70,8 @@ Requires Python 3.8+ and a Universal Robots e-Series (UR3e to UR30).
70
70
 
71
71
  ### Platform Support
72
72
 
73
- - **Linux, macOS**: fully supported
74
- - **Windows**: the Python library works on Windows. The interactive CLIs (`urkit teach`, `urkit points`) run on Windows but are **not fully tested and not officially supported**. A warning is printed at startup. If you hit input or console problems, use Linux, macOS, or WSL2
73
+ - **Linux, Windows**: fully supported. The interactive CLIs (`urkit teach`, `urkit points`) are tested on both platforms
74
+ - **macOS**: runs the same POSIX terminal code path as Linux (termios/select); there is no macOS-specific code
75
75
 
76
76
  ### Robot Setup (one-time)
77
77
 
@@ -112,7 +112,7 @@ That's it. No `.urp` files to run, no extra programs needed.
112
112
  If you have a Robotiq gripper, install the **Robotiq Gripper Control** URCap first: download from [robotiq.com/support](https://robotiq.com/support), copy the `.urcap` to a USB drive, and install via `☰` → `Settings` → `System` → `URCaps`.
113
113
 
114
114
  ```python
115
- from urkit import URRobot, ROBOTIQ_HAND_E # or ROBOTIQ_2F_85, ROBOTIQ_2F_140, or gripper=None
115
+ from urkit import URRobot, ROBOTIQ_HAND_E # or ROBOTIQ_2F_85, ROBOTIQ_2F_140, ROBOTIQ_EPICK_SINGLE_S, or gripper=None
116
116
 
117
117
  robot = URRobot(ip="192.168.1.50", points="points.db", gripper=ROBOTIQ_HAND_E)
118
118
  robot.gripper.activate()
@@ -120,8 +120,10 @@ robot.gripper.activate()
120
120
  robot.move_to("home")
121
121
  robot.move_to("pick", dz=0.05) # 5cm above
122
122
  robot.gripper.close()
123
+ robot.set_held_payload(0.05) # picked up 50 g: arm compensates gravity
123
124
  robot.move_to("place")
124
125
  robot.gripper.open()
126
+ robot.set_held_payload(0) # released: back to gripper-only payload
125
127
  ```
126
128
 
127
129
  The typical workflow:
@@ -169,12 +171,14 @@ The CLI auto-loads `config.yaml` from the current working directory. Pass `--con
169
171
  |-----|-------------|---------|
170
172
  | `robot_ip` | Robot IP address | `192.168.1.50` |
171
173
  | `points_path` | Path to SQLite points database | `points.db` |
172
- | `gripper` | Gripper preset name | `hand-e`, `2f-85`, `2f-140`, `digital` |
174
+ | `gripper` | Gripper preset name | `hand-e`, `2f-85`, `2f-140`, `epick-single-s`, `digital` |
173
175
  | `ik_reference` | IK reference posture (prevents elbow flipping) | `home` |
174
176
  | `default_vel` | Default linear velocity (m/s) | `0.5` |
175
177
  | `default_acc` | Default linear acceleration (m/s²) | `0.3` |
176
178
  | `expert_mode` | Disable safety speed clamping | `false` |
177
179
 
180
+ The loader fails loudly with a reason when a config file exists but is wrong: malformed YAML (file + parse error), a document that is not a mapping, or unknown top-level keys (offending keys + the valid ones; usually a typo). The table above is the complete key set. The only silent case is a missing file, where the CLI runs on flags and built-in defaults.
181
+
178
182
  ### Gripper Config
179
183
 
180
184
  Built-in preset with overrides:
@@ -265,8 +269,6 @@ urkit init --force # overwrite if exists
265
269
 
266
270
  ### Teach Mode
267
271
 
268
- ### Teach Mode
269
-
270
272
  The interactive teach pendant for moving the robot, saving points, and checking telemetry:
271
273
 
272
274
  ```bash
@@ -279,10 +281,10 @@ urkit teach # reads IP from config.yaml
279
281
  | Flag | Description |
280
282
  |------|-------------|
281
283
  | `ip` | Robot IP address (positional, overrides config) |
282
- | `--gripper` | Gripper preset: `2f-85`, `2f-140`, `hand-e`, `digital`, `none` |
284
+ | `--gripper` | Gripper preset: `2f-85`, `2f-140`, `hand-e`, `epick-single-s`, `digital`, `none` |
283
285
  | `--gripper-pin` | Digital gripper output pin (default: 0) |
284
- | `--gripper-force` | Robotiq force 0-100 |
285
- | `--gripper-speed` | Robotiq speed 0-100 |
286
+ | `--gripper-force` | 2F grip force 0-100 (EPick: use `max_vacuum` in `gripper_config`) |
287
+ | `--gripper-speed` | 2F movement speed 0-100 (EPick: use `grip_timeout` in `gripper_config`) |
286
288
  | `--gripper-close-on-high` | Digital polarity: `true` or `false` |
287
289
  | `--points` | Path to `points.db` file (overrides config) |
288
290
  | `--config` | Path to config file (default: `config.yaml` in project root or CWD) |
@@ -348,9 +350,10 @@ All movement and orientation keys support **hold-to-repeat**.
348
350
  <tr><th>Key</th><th>Action</th></tr>
349
351
  <tr><td><code>X</code></td><td>Open</td></tr>
350
352
  <tr><td><code>C</code></td><td>Close</td></tr>
351
- <tr><td><code>V</code></td><td>Set position (mm)</td></tr>
352
- <tr><td><code>6</code></td><td>Set speed (0-100)</td></tr>
353
- <tr><td><code>7</code></td><td>Set force (0-100)</td></tr>
353
+ <tr><td><code>V</code></td><td>Set position % (2F only; hidden for the EPick)</td></tr>
354
+ <tr><td><code>6</code></td><td>Set speed (2F) / min vacuum (EPick)</td></tr>
355
+ <tr><td><code>7</code></td><td>Set force (2F) / max vacuum (EPick)</td></tr>
356
+ <tr><td><code>9</code></td><td>Set held payload (kg, 0 = reset to base)</td></tr>
354
357
  </table>
355
358
  </td>
356
359
  <td align="center" style="width:33%">
@@ -383,10 +386,10 @@ All movement and orientation keys support **hold-to-repeat**.
383
386
  The teach pendant shows live joint angles alongside TCP position and orientation:
384
387
 
385
388
  ```
386
- Position X=+0.432 Y=+0.111 Z=+0.227
387
- Orientation R=+131.3 P=-121.0 Y= +8.0
388
- Joints J1=+150.0 J2=+ 20.0 J3=+160.0
389
- J4=+ 50.0 J5=- 80.0 J6=+157.0
389
+ Position (mm) X= +432.0 Y= +111.0 Z= +227.0
390
+ Orientation R= +131.3 P= -121.0 Y= +8.0
391
+ Joints J1=+150.0 J2= +20.0 J3=+160.0
392
+ J4= +50.0 J5= -80.0 J6=+157.0
390
393
  ```
391
394
 
392
395
  Joint angles color-code proximity to mechanical limits:
@@ -454,13 +457,16 @@ The constructor takes a few seconds on first call: it validates the connection,
454
457
 
455
458
  ### Grippers
456
459
 
457
- Three built-in presets:
460
+ Built-in presets:
458
461
 
459
462
  | Preset | Description |
460
463
  |--------|-------------|
461
464
  | `ROBOTIQ_HAND_E` | Robotiq Hand-E (2F-85-E) |
462
465
  | `ROBOTIQ_2F_85` | Robotiq 2F-85 |
463
466
  | `ROBOTIQ_2F_140` | Robotiq 2F-140 |
467
+ | `ROBOTIQ_EPICK_SINGLE_S/L` | EPick + 1 cup (S = Ø40, L = Ø55) |
468
+ | `ROBOTIQ_EPICK_DUAL_S/L` | EPick + 2-cup bracket (S = Ø40, L = Ø55) |
469
+ | `ROBOTIQ_EPICK_QUAD_S/L` | EPick + 4-cup bracket (S = Ø40, L = Ø55) |
464
470
 
465
471
  ```python
466
472
  robot.gripper.activate() # required before open/close (Robotiq only)
@@ -471,7 +477,7 @@ robot.gripper.open() # fully open (blocking by default)
471
477
  robot.gripper.close() # fully closed, stops on contact
472
478
  robot.gripper.open(wait=False) # non-blocking return (waits for position)
473
479
  robot.gripper.set_position_mm(20) # 20mm open (Robotiq only, 0 = fully closed)
474
- robot.gripper.set_position_percent(50) # 50% open (Robotiq only, 0 = fully open, 100 = fully closed)
480
+ robot.gripper.set_position_percent(50) # 50% open (2F; repurposed on the EPick, see below)
475
481
  robot.gripper.set_force(50) # grip force: 0-100 (Robotiq only)
476
482
  robot.gripper.set_speed(80) # movement speed: 0-100 (Robotiq only)
477
483
 
@@ -517,6 +523,83 @@ robot.gripper.close() # turn pin 3 on
517
523
 
518
524
  `set_force()` and `set_speed()` are not available for digital grippers.
519
525
 
526
+ #### EPick Vacuum Gripper
527
+
528
+ The EPick presets drive a Robotiq EPick vacuum gripper (e-Series only) over the same RS485 Robotiq protocol as the 2F series, using the legacy Robotiq ASCII commands. They require the **Robotiq Gripper Control** URCap installed (same setup as the 2F presets).
529
+
530
+ The semantics are inverted compared to the 2F grippers:
531
+
532
+ ```python
533
+ robot.gripper.activate() # required before open/close
534
+ robot.gripper.close() # grip: pump on, wait until an object is secured
535
+ robot.gripper.open() # release: pump off + blow-off, wait until the object is gone
536
+ ```
537
+
538
+ `close()` does not return until the gripper has confirmed a secured object (see Detection below), and raises `GripperError` if no object is detected within the grip timeout. A failed grip leaves the vacuum on, so call `open()` to release before moving again. `open()` waits until the vacuum is lost (the object has dropped).
539
+
540
+ Grip parameters (set at connect time, or changed between grips):
541
+
542
+ ```python
543
+ robot.gripper.set_max_vacuum(60) # max vacuum level, 20-100% (default 60, the Robotiq URCap default)
544
+ robot.gripper.set_min_vacuum(40) # min vacuum level, 10% up to the max vacuum (default 40)
545
+ robot.gripper.set_grip_timeout(30) # grip timeout in 100 ms units, 0-100 (default 30 = 3 s)
546
+ ```
547
+
548
+ The ranges are enforced at construction and on every setter (max 20-100, min 10 up to the current max, timeout 0-100), and a parameter of one backend passed to another raises `GripperError` instead of being ignored (`force`/`speed` on the EPick, `max_vacuum`/`grip_timeout` on the 2F). The same names work in `config.yaml`:
549
+
550
+ ```yaml
551
+ gripper: epick-single-s
552
+ gripper_config:
553
+ max_vacuum: 70
554
+ grip_timeout: 40
555
+ ```
556
+
557
+ With `set_max_vacuum(100)` the pump runs continuously and the gripper cannot confirm a grip; keep the max vacuum below 100% (60 works for typical cups and trays). The same physics applies at lower levels: if the max vacuum is higher than a leaky seal can reach, the pump keeps hunting and never enters the stable hold phase where the secured flag appears, so the grip times out (verified: a marginal-seal object gripped at 60/40 but timed out at 80/50 and 80/30). For leaky seals keep `max_vacuum` low and `min_vacuum` at or below what the seal sustains, not the other way around.
558
+
559
+ `set_position_mm()`, `get_position_mm()`, and `max_travel_mm()` are not available: the EPick has no finger travel. `set_position_percent(p)` is repurposed as a one-shot vacuum grip: `p=0` releases, `1-100` grips at that vacuum level with the normal secured-object wait (the teach pendant does not expose this; use [6]/[7] and C).
560
+
561
+ **Payload and TCP.** The bare generator body is 0.706 kg, CoG `[0, 0, 0.0515]`, TCP `[0, 0, 0.113, 0, 0, 0]` (Robotiq EPick manual, Table 6-4); cups and brackets change all three. The kits ship cups in two sizes (Ø 40 mm and Ø 55 mm), so there is one preset per bracket system + cup size (configurations without cups cannot grip and have no preset):
562
+
563
+ | Preset | Config / CLI name | Setup |
564
+ |--------|-------------------|-------|
565
+ | `ROBOTIQ_EPICK_SINGLE_S` | `epick-single-s` | 1 small (Ø40) cup on the generator port |
566
+ | `ROBOTIQ_EPICK_SINGLE_L` | `epick-single-l` | 1 large (Ø55) cup on the generator port |
567
+ | `ROBOTIQ_EPICK_DUAL_S` | `epick-dual-s` | 2-cup bracket, small cups |
568
+ | `ROBOTIQ_EPICK_DUAL_L` | `epick-dual-l` | 2-cup bracket, large cups |
569
+ | `ROBOTIQ_EPICK_QUAD_S` | `epick-quad-s` | 4-cup bracket, small cups |
570
+ | `ROBOTIQ_EPICK_QUAD_L` | `epick-quad-l` | 4-cup bracket, large cups |
571
+
572
+ ```python
573
+ from urkit import URRobot, ROBOTIQ_EPICK_SINGLE_S
574
+
575
+ robot = URRobot(ip="192.168.1.50", gripper=ROBOTIQ_EPICK_SINGLE_S)
576
+ ```
577
+
578
+ The TCP z of each preset is the bottom of the cups, the limiting point for approach moves. The presets use the manual's Table 6-4 for the body and bracket stack, and the official Festo datasheets for the cups Robotiq ships (VASB-40-1/4-PUR-B: 30 g / 32 mm, VASB-55-1/4-PUR-B: 39 g / 39 mm); all-small combinations reproduce Table 6-4 exactly:
579
+
580
+ | Preset | Mass (kg) | CoG z (m) | TCP z (m) |
581
+ |--------|-----------|-----------|-----------|
582
+ | body only (reference) | 0.706 | 0.0515 | 0.113 |
583
+ | `ROBOTIQ_EPICK_SINGLE_S` | 0.736 | 0.0547 | 0.145 |
584
+ | `ROBOTIQ_EPICK_SINGLE_L` | 0.745 | 0.0557 | 0.152 |
585
+ | `ROBOTIQ_EPICK_DUAL_S` | 1.116 | 0.0819 | 0.196 |
586
+ | `ROBOTIQ_EPICK_DUAL_L` | 1.134 | 0.0837 | 0.203 |
587
+ | `ROBOTIQ_EPICK_QUAD_S` | 1.341 | 0.0920 | 0.196 |
588
+ | `ROBOTIQ_EPICK_QUAD_L` | 1.377 | 0.0947 | 0.203 |
589
+
590
+ Bracket mass is derived (±15 g) and the Ø55 height is CAD-derived (±2 mm); if your setup is critical, measure the actual stack and pass `mass` / `center_of_gravity` / `tcp_offset` overrides, which still work:
591
+
592
+ ```python
593
+ robot = URRobot(
594
+ ip="192.168.1.50",
595
+ gripper=ROBOTIQ_EPICK_SINGLE_S,
596
+ mass=0.74,
597
+ tcp_offset=[0.0, 0.0, 0.147, 0.0, 0.0, 0.0],
598
+ )
599
+ ```
600
+
601
+ **Detection.** The EPick runs in advanced mode (rMOD=1): the pump reaches the max vacuum level, stops, and the vacuum decays back to the min level. An object is only "secured" while the vacuum is actually held there, so `close()` returns as soon as that secured state is observed on the gripper's object flag. With nothing under the suction cup the vacuum collapses and the secured state never occurs, so `close()` times out and raises `GripperError` after the grip timeout (set via `grip_timeout`). In automatic mode (rMOD=0) the EPick reports a short "seemingly constant" vacuum as an object, which false-positives with no object on the cup, so urkit does not use it.
602
+
520
603
  ### Points & Motion
521
604
 
522
605
  The points database is optional. Create a robot without one and attach later:
@@ -747,7 +830,7 @@ robot.speed_slider = 0.5 # 50% velocity cap
747
830
  robot.speed_slider # read current slider (0.0-1.0)
748
831
  ```
749
832
 
750
- The speed slider controls the pendant's speed multiplier. It's global, persistent, and affects all motion commands.
833
+ The speed slider controls the pendant's speed multiplier. It's global, persistent (stored on the robot, survives restarts), and affects all motion commands. Because 0% persists as a silent freeze of all motion, the pendant's `[0]` prompt rejects 0 (1-100 only); a deliberate freeze is still possible via the API (`robot.speed_slider = 0`).
751
834
 
752
835
  #### Changing Default Speed & Acceleration
753
836
 
@@ -883,6 +966,27 @@ robot.tcp_offset = [0, 0, 0.15, 0, 0, 0]
883
966
  robot.set_payload(1.5, [0, 0, 0.05]) # mass (kg), center of gravity [x, y, z]
884
967
  ```
885
968
 
969
+ **Held payload.** The controller compensates gravity using the payload, so add the mass of a picked-up object before moving it, and reset after release:
970
+
971
+ ```python
972
+ robot.gripper.close()
973
+ robot.set_held_payload(0.05) # holding 50 g, object CoG at the TCP
974
+ robot.set_held_payload(0.05, cog=[0, 0, 0.25]) # object hangs below the flange
975
+ robot.set_held_payload(0) # released: back to the base payload
976
+
977
+ robot.base_payload # gripper + tooling, kg (no held object)
978
+ robot.held_payload # currently held mass, kg (0 = none)
979
+ robot.payload # total (base + held), kg
980
+ ```
981
+
982
+ `set_held_payload(0)` resets to the base payload captured at connect (the gripper preset plus any `mass=` / `center_of_gravity` / `tcp_offset` overrides), never to a literal zero. The combined CoG is the mass-weighted average of the base CoG and the object's CoG (tool coordinates, origin at the flange). A held object that extends the tool does not need a TCP change: give its CoG the deeper z instead.
983
+
984
+ The base payload can be changed at runtime with `set_payload(...)`: while nothing is held, a manual `set_payload` call becomes the new base (custom fingers, a re-weighed tool). While a payload is held, `set_payload` raises, since the mass would be ambiguous: call `set_held_payload(0)` first.
985
+
986
+ In the teach pendant, **9** sets the held payload (prompt: `Held payload in kg (0 = reset to base)`); the Payload status row shows the held mass and hints the key, e.g. `1.427kg +0.050 held [9]`. Useful when teaching points while gripping a real part.
987
+
988
+ **How much to report.** There is no enforced minimum, and over-reporting is harmless. Practical guidance: below ~10 g the sag is sub-millimeter on any e-Series arm and not worth it; from ~50 g up (roughly 1% of a UR5e's rated payload) report it, since full-reach sag reaches several mm; in between, report it for slow precision work (insertion, contact moves) and skip it for coarse moves.
989
+
886
990
  Clean shutdown:
887
991
 
888
992
  ```python
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "urkit"
7
- version = "0.5.0"
7
+ version = "0.6.0"
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.5.0"
27
+ __version__ = "0.6.0"
28
28
 
29
29
 
30
30
  from urkit.exceptions import (
@@ -55,6 +55,12 @@ from urkit.gripper.presets import (
55
55
  PRESETS,
56
56
  ROBOTIQ_2F_85,
57
57
  ROBOTIQ_2F_140,
58
+ ROBOTIQ_EPICK_DUAL_L,
59
+ ROBOTIQ_EPICK_DUAL_S,
60
+ ROBOTIQ_EPICK_QUAD_L,
61
+ ROBOTIQ_EPICK_QUAD_S,
62
+ ROBOTIQ_EPICK_SINGLE_L,
63
+ ROBOTIQ_EPICK_SINGLE_S,
58
64
  ROBOTIQ_HAND_E,
59
65
  )
60
66
  from urkit.motion import FreedriveMode
@@ -76,6 +82,12 @@ __all__ = [
76
82
  "ROBOTIQ_2F_85",
77
83
  "ROBOTIQ_2F_140",
78
84
  "ROBOTIQ_HAND_E",
85
+ "ROBOTIQ_EPICK_SINGLE_S",
86
+ "ROBOTIQ_EPICK_SINGLE_L",
87
+ "ROBOTIQ_EPICK_DUAL_S",
88
+ "ROBOTIQ_EPICK_DUAL_L",
89
+ "ROBOTIQ_EPICK_QUAD_S",
90
+ "ROBOTIQ_EPICK_QUAD_L",
79
91
  # Freedrive mode
80
92
  "FreedriveMode",
81
93
  # Move frame
@@ -61,7 +61,8 @@ def main() -> None:
61
61
  type=str,
62
62
  default=None,
63
63
  help="Gripper preset (overrides config file). Use 'none' to disable gripper. "
64
- "Accepts: 2f-85, 2f-140, hand-e, digital, none (case-insensitive, _ or -)",
64
+ "Accepts: 2f-85, 2f-140, hand-e, epick-<system>-<cup>, digital, none "
65
+ "(case-insensitive, _ or -)",
65
66
  )
66
67
  teach_parser.add_argument(
67
68
  "--gripper-pin",
@@ -73,13 +74,15 @@ def main() -> None:
73
74
  "--gripper-force",
74
75
  type=int,
75
76
  default=None,
76
- help="Robotiq gripper force 0-100 (overrides config file)",
77
+ help="2F grip force 0-100 (EPick: use max_vacuum in the config's gripper_config) "
78
+ "(overrides config file)",
77
79
  )
78
80
  teach_parser.add_argument(
79
81
  "--gripper-speed",
80
82
  type=int,
81
83
  default=None,
82
- help="Robotiq gripper speed 0-100 (overrides config file)",
84
+ help="2F movement speed 0-100 (EPick: use grip_timeout in the config's gripper_config) "
85
+ "(overrides config file)",
83
86
  )
84
87
  teach_parser.add_argument(
85
88
  "--gripper-close-on-high",
@@ -29,7 +29,7 @@ robot_ip: 192.168.1.50
29
29
  points_path: points.db
30
30
 
31
31
  # Gripper preset (optional)
32
- # Built-in: hand-e, 2f-85, 2f-140
32
+ # Built-in: hand-e, 2f-85, 2f-140, epick-<system>-<cup> (e.g. epick-quad-l)
33
33
  # Digital I/O: digital
34
34
  # Omit or set to none for no gripper
35
35
  gripper: none
@@ -41,11 +41,18 @@ gripper_config:
41
41
  # center_of_gravity: [0.0, 0.0, 0.0] # [x, y, z] in meters
42
42
  # tcp_offset: [0.0, 0.0, 0.175, 0.0, 0.0, 0.0] # [x, y, z, rx, ry, rz]
43
43
 
44
- # Robotiq-specific (hand-e, 2f-85, 2f-140)
44
+ # 2F grippers (hand-e, 2f-85, 2f-140)
45
45
  # force: 100 # grip force 0-100
46
46
  # speed: 100 # movement speed 0-100
47
47
  # max_mm: 85 # max finger travel (e.g. 85 for 2F-85)
48
48
 
49
+ # EPick (vacuum): max_vacuum 20-100, min_vacuum 10 up to max_vacuum,
50
+ # grip_timeout 0-100 (x100 ms). A 2F param (force/speed) on an EPick
51
+ # preset raises an error and vice versa.
52
+ # max_vacuum: 60
53
+ # min_vacuum: 40
54
+ # grip_timeout: 30
55
+
49
56
  # Digital gripper
50
57
  # pin: 0 # digital output pin
51
58
  # close_on_high: true # true=HIGH closes, false=LOW closes
@@ -19,7 +19,7 @@ from rich.table import Table
19
19
  from urkit import __version__
20
20
  from urkit.config import _load_config as load_config
21
21
  from urkit.cli.colors import blue, cyan, dim, yellow
22
- from urkit.cli.terminal import RawTerminal, read_burst, wait_input, warn_if_windows
22
+ from urkit.cli.terminal import RawTerminal, read_burst, wait_input
23
23
  from urkit.points import Points
24
24
 
25
25
 
@@ -29,7 +29,6 @@ def points_command(args) -> None:
29
29
  Args:
30
30
  args: Parsed arguments from argparse (with points subcommand attributes).
31
31
  """
32
- warn_if_windows()
33
32
  _explore_points(args)
34
33
 
35
34